From 27cfc8a476911e2df559ac2a15dc66ed6365aea0 Mon Sep 17 00:00:00 2001
From: Matthias Stock
Date: Wed, 8 Mar 2017 23:41:35 +0100
Subject: [PATCH] Products From Database
- Categories
- Products
- i18n
- Templating
---
build.gradle | 3 +
config/checkstyle/checkstyle.xml | 4 +-
.../monolith/config/HibernateConfig.java | 15 +++
.../de/mstock/monolith/config/I18nConfig.java | 32 +++--
.../de/mstock/monolith/domain/Category.java | 61 ++++++++++
.../mstock/monolith/domain/CategoryI18n.java | 37 ++++++
.../monolith/domain/CategoryI18nPk.java | 52 ++++++++
.../monolith/domain/CategoryRepository.java | 18 +++
.../de/mstock/monolith/domain/Product.java | 58 +++++++++
.../mstock/monolith/domain/ProductI18n.java | 64 ++++++++++
.../mstock/monolith/domain/ProductI18nPk.java | 52 ++++++++
.../monolith/domain/ProductRepository.java | 12 ++
.../monolith/domain/ProductWeightUnit.java | 5 +
.../monolith/service/NotFoundException.java | 11 ++
.../mstock/monolith/service/ShopService.java | 85 +++++++++++++
.../monolith/web/CategoryController.java | 25 +++-
.../de/mstock/monolith/web/CategoryDTO.java | 21 ++++
.../monolith/web/HomepageController.java | 18 ++-
.../monolith/web/ProductController.java | 23 +++-
.../de/mstock/monolith/web/ProductDTO.java | 57 +++++++++
src/main/resources/application.properties | 3 +
.../migration/V1__create_initial_schema.sql | 37 ++++++
.../db/migration/V2__insert_data.sql | 41 +++++++
src/main/resources/messages.properties | 11 +-
src/main/resources/messages_de.properties | 11 +-
src/main/resources/templates/category.html | 113 +-----------------
.../templates/fragments/skeleton.html | 9 +-
src/main/resources/templates/homepage.html | 6 +-
src/main/resources/templates/product.html | 6 +-
.../MonolithApplicationTests.java | 8 +-
.../monolith/service/ShopServiceTest.java | 64 ++++++++++
31 files changed, 802 insertions(+), 160 deletions(-)
create mode 100644 src/main/java/de/mstock/monolith/config/HibernateConfig.java
create mode 100644 src/main/java/de/mstock/monolith/domain/Category.java
create mode 100644 src/main/java/de/mstock/monolith/domain/CategoryI18n.java
create mode 100644 src/main/java/de/mstock/monolith/domain/CategoryI18nPk.java
create mode 100644 src/main/java/de/mstock/monolith/domain/CategoryRepository.java
create mode 100644 src/main/java/de/mstock/monolith/domain/Product.java
create mode 100644 src/main/java/de/mstock/monolith/domain/ProductI18n.java
create mode 100644 src/main/java/de/mstock/monolith/domain/ProductI18nPk.java
create mode 100644 src/main/java/de/mstock/monolith/domain/ProductRepository.java
create mode 100644 src/main/java/de/mstock/monolith/domain/ProductWeightUnit.java
create mode 100644 src/main/java/de/mstock/monolith/service/NotFoundException.java
create mode 100644 src/main/java/de/mstock/monolith/service/ShopService.java
create mode 100644 src/main/java/de/mstock/monolith/web/CategoryDTO.java
create mode 100644 src/main/java/de/mstock/monolith/web/ProductDTO.java
create mode 100644 src/main/resources/db/migration/V1__create_initial_schema.sql
create mode 100644 src/main/resources/db/migration/V2__insert_data.sql
rename src/test/java/de/mstock/{ => monolith}/MonolithApplicationTests.java (68%)
create mode 100644 src/test/java/de/mstock/monolith/service/ShopServiceTest.java
diff --git a/build.gradle b/build.gradle
index 4fc4331..39cec65 100644
--- a/build.gradle
+++ b/build.gradle
@@ -50,7 +50,10 @@ dependencies {
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.springframework.boot:spring-boot-starter-web')
+ compile('org.apache.commons:commons-lang3:3.5')
runtime('org.springframework.boot:spring-boot-devtools')
runtime('org.postgresql:postgresql')
testCompile('org.springframework.boot:spring-boot-starter-test')
+ testCompile('org.mockito:mockito-core:2.7.14')
+ testCompile('org.hamcrest:hamcrest-core:1.3')
}
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
index a984474..8fc903a 100644
--- a/config/checkstyle/checkstyle.xml
+++ b/config/checkstyle/checkstyle.xml
@@ -159,12 +159,12 @@
-
+
-
+
diff --git a/src/main/java/de/mstock/monolith/config/HibernateConfig.java b/src/main/java/de/mstock/monolith/config/HibernateConfig.java
new file mode 100644
index 0000000..c69a2ab
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/config/HibernateConfig.java
@@ -0,0 +1,15 @@
+package de.mstock.monolith.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.orm.jpa.vendor.HibernateJpaSessionFactoryBean;
+
+@Configuration
+public class HibernateConfig {
+
+ @Bean
+ public HibernateJpaSessionFactoryBean sessionFactory() {
+ return new HibernateJpaSessionFactoryBean();
+ }
+
+}
diff --git a/src/main/java/de/mstock/monolith/config/I18nConfig.java b/src/main/java/de/mstock/monolith/config/I18nConfig.java
index e18191b..4e63724 100644
--- a/src/main/java/de/mstock/monolith/config/I18nConfig.java
+++ b/src/main/java/de/mstock/monolith/config/I18nConfig.java
@@ -1,34 +1,32 @@
package de.mstock.monolith.config;
+import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
-import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
-import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
-import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
@Configuration
public class I18nConfig extends WebMvcConfigurerAdapter {
+ private static final List SUPPORTED_LOCALES =
+ Arrays.asList(new Locale("de_DE"), new Locale("en_US"));
+
+ /**
+ * Creates a Bean, managed by Spring.
+ *
+ * @return A configured LocaleResolver
+ */
@Bean
public LocaleResolver localeResolver() {
- SessionLocaleResolver slr = new SessionLocaleResolver();
- slr.setDefaultLocale(Locale.US);
- return slr;
+ AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
+ localeResolver.setSupportedLocales(SUPPORTED_LOCALES);
+ localeResolver.setDefaultLocale(Locale.US);
+ return localeResolver;
}
- @Bean
- public LocaleChangeInterceptor localeChangeInterceptor() {
- LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
- lci.setParamName("lang");
- return lci;
- }
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(localeChangeInterceptor());
- }
}
diff --git a/src/main/java/de/mstock/monolith/domain/Category.java b/src/main/java/de/mstock/monolith/domain/Category.java
new file mode 100644
index 0000000..b7e1abf
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/Category.java
@@ -0,0 +1,61 @@
+package de.mstock.monolith.domain;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.MapKey;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "categories")
+public class Category {
+
+ @Id
+ private int id;
+ private int ordinal;
+ @OneToMany(mappedBy = "category")
+ @MapKey(name = "categoryI18nPk.localeLanguage")
+ private Map i18n;
+ @ManyToMany
+ @JoinTable(name = "category_products", inverseJoinColumns = @JoinColumn(name = "product_id"))
+ private List products;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public int getOrdinal() {
+ return ordinal;
+ }
+
+ public void setOrdinal(int ordinal) {
+ this.ordinal = ordinal;
+ }
+
+ public Map getI18n() {
+ return i18n;
+ }
+
+ public void setI18n(Map i18n) {
+ this.i18n = i18n;
+ }
+
+ public List getProducts() {
+ return products;
+ }
+
+ public void setProducts(List products) {
+ this.products = products;
+ }
+
+}
diff --git a/src/main/java/de/mstock/monolith/domain/CategoryI18n.java b/src/main/java/de/mstock/monolith/domain/CategoryI18n.java
new file mode 100644
index 0000000..379dea2
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/CategoryI18n.java
@@ -0,0 +1,37 @@
+package de.mstock.monolith.domain;
+
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import javax.persistence.ManyToOne;
+import javax.persistence.MapsId;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "categories_i18n")
+public class CategoryI18n {
+
+ @EmbeddedId
+ private CategoryI18nPk categoryI18nPk;
+ @ManyToOne
+ @MapsId("categoryId")
+ private Category category;
+ private String name;
+ private String prettyUrlFragment;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getPrettyUrlFragment() {
+ return prettyUrlFragment;
+ }
+
+ public void setPrettyUrlFragment(String prettyUrlFragment) {
+ this.prettyUrlFragment = prettyUrlFragment;
+ }
+
+}
diff --git a/src/main/java/de/mstock/monolith/domain/CategoryI18nPk.java b/src/main/java/de/mstock/monolith/domain/CategoryI18nPk.java
new file mode 100644
index 0000000..faf2bfc
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/CategoryI18nPk.java
@@ -0,0 +1,52 @@
+package de.mstock.monolith.domain;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.persistence.Embeddable;
+
+@Embeddable
+public class CategoryI18nPk implements Serializable {
+
+ private static final long serialVersionUID = 6985884171403931363L;
+ private int categoryId;
+ private String localeLanguage;
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(categoryId, localeLanguage);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ CategoryI18nPk rhs = (CategoryI18nPk) obj;
+ return Objects.equals(categoryId, rhs.categoryId)
+ && Objects.equals(localeLanguage, rhs.localeLanguage);
+ }
+
+ public int getCategoryId() {
+ return categoryId;
+ }
+
+ public void setCategoryId(int categoryId) {
+ this.categoryId = categoryId;
+ }
+
+ public String getLocaleLanguage() {
+ return localeLanguage;
+ }
+
+ public void setLocaleLanguage(String localeLanguage) {
+ this.localeLanguage = localeLanguage;
+ }
+
+}
diff --git a/src/main/java/de/mstock/monolith/domain/CategoryRepository.java b/src/main/java/de/mstock/monolith/domain/CategoryRepository.java
new file mode 100644
index 0000000..bad86af
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/CategoryRepository.java
@@ -0,0 +1,18 @@
+package de.mstock.monolith.domain;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.Repository;
+
+public interface CategoryRepository extends Repository {
+
+ @Query("select distinct c from Category c join fetch c.i18n i "
+ + "where key(i) = ?1 order by c.ordinal")
+ List findAllOrdered(String language);
+
+ @Query("select c from Category c join fetch c.i18n i join fetch c.products "
+ + "where key(i) = ?1 and lower(i.prettyUrlFragment) = ?2")
+ Category findByPrettyUrlFragment(String language, String prettyUrlFragment);
+
+}
diff --git a/src/main/java/de/mstock/monolith/domain/Product.java b/src/main/java/de/mstock/monolith/domain/Product.java
new file mode 100644
index 0000000..52e5131
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/Product.java
@@ -0,0 +1,58 @@
+package de.mstock.monolith.domain;
+
+import java.util.Map;
+
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Id;
+import javax.persistence.MapKey;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "products")
+public class Product {
+
+ @Id
+ private int id;
+ private String itemNumber;
+ @Enumerated(EnumType.ORDINAL)
+ private ProductWeightUnit unit;
+ @OneToMany(mappedBy = "product")
+ @MapKey(name = "productI18nPk.localeLanguage")
+ private Map i18n;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getItemNumber() {
+ return itemNumber;
+ }
+
+ public void setItemNumber(String itemNumber) {
+ this.itemNumber = itemNumber;
+ }
+
+ public ProductWeightUnit getUnit() {
+ return unit;
+ }
+
+ public void setUnit(ProductWeightUnit unit) {
+ this.unit = unit;
+ }
+
+ public Map getI18n() {
+ return i18n;
+ }
+
+ public void setI18n(Map i18n) {
+ this.i18n = i18n;
+ }
+
+}
diff --git a/src/main/java/de/mstock/monolith/domain/ProductI18n.java b/src/main/java/de/mstock/monolith/domain/ProductI18n.java
new file mode 100644
index 0000000..3ccc205
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/ProductI18n.java
@@ -0,0 +1,64 @@
+package de.mstock.monolith.domain;
+
+import java.math.BigDecimal;
+
+import javax.persistence.EmbeddedId;
+import javax.persistence.Entity;
+import javax.persistence.ManyToOne;
+import javax.persistence.MapsId;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "products_i18n")
+public class ProductI18n {
+
+ @EmbeddedId
+ private ProductI18nPk productI18nPk;
+ @ManyToOne
+ @MapsId("productId")
+ private Product product;
+ private String name;
+ private String prettyUrlFragment;
+ private BigDecimal price;
+ private String description;
+
+ public ProductI18nPk getProductI18nPk() {
+ return productI18nPk;
+ }
+
+ public void setProductI18nPk(ProductI18nPk productI18nPk) {
+ this.productI18nPk = productI18nPk;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getPrettyUrlFragment() {
+ return prettyUrlFragment;
+ }
+
+ public void setPrettyUrlFragment(String prettyUrlFragment) {
+ this.prettyUrlFragment = prettyUrlFragment;
+ }
+
+ public BigDecimal getPrice() {
+ return price;
+ }
+
+ public void setPrice(BigDecimal price) {
+ this.price = price;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/src/main/java/de/mstock/monolith/domain/ProductI18nPk.java b/src/main/java/de/mstock/monolith/domain/ProductI18nPk.java
new file mode 100644
index 0000000..90e72aa
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/ProductI18nPk.java
@@ -0,0 +1,52 @@
+package de.mstock.monolith.domain;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.persistence.Embeddable;
+
+@Embeddable
+public class ProductI18nPk implements Serializable {
+
+ private static final long serialVersionUID = 4916705045560501228L;
+ private int productId;
+ private String localeLanguage;
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(productId, localeLanguage);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ProductI18nPk rhs = (ProductI18nPk) obj;
+ return Objects.equals(productId, rhs.productId)
+ && Objects.equals(localeLanguage, rhs.localeLanguage);
+ }
+
+ public int getProductId() {
+ return productId;
+ }
+
+ public void setProductId(int productId) {
+ this.productId = productId;
+ }
+
+ public String getLocaleLanguage() {
+ return localeLanguage;
+ }
+
+ public void setLocaleLanguage(String localeLanguage) {
+ this.localeLanguage = localeLanguage;
+ }
+
+}
diff --git a/src/main/java/de/mstock/monolith/domain/ProductRepository.java b/src/main/java/de/mstock/monolith/domain/ProductRepository.java
new file mode 100644
index 0000000..b0259e0
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/ProductRepository.java
@@ -0,0 +1,12 @@
+package de.mstock.monolith.domain;
+
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.Repository;
+
+public interface ProductRepository extends Repository {
+
+ @Query("select p from Product p join fetch p.i18n i "
+ + "where key(i) = ?1 and lower(i.name) = ?2")
+ Product findByI18nName(String language, String name);
+
+}
diff --git a/src/main/java/de/mstock/monolith/domain/ProductWeightUnit.java b/src/main/java/de/mstock/monolith/domain/ProductWeightUnit.java
new file mode 100644
index 0000000..7e7c0f7
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/domain/ProductWeightUnit.java
@@ -0,0 +1,5 @@
+package de.mstock.monolith.domain;
+
+public enum ProductWeightUnit {
+ UNKNOWN, PIECE, GRAMM_100, KILO_1
+}
diff --git a/src/main/java/de/mstock/monolith/service/NotFoundException.java b/src/main/java/de/mstock/monolith/service/NotFoundException.java
new file mode 100644
index 0000000..76c277e
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/service/NotFoundException.java
@@ -0,0 +1,11 @@
+package de.mstock.monolith.service;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.NOT_FOUND)
+public class NotFoundException extends RuntimeException {
+
+ private static final long serialVersionUID = 4799626622256800091L;
+
+}
diff --git a/src/main/java/de/mstock/monolith/service/ShopService.java b/src/main/java/de/mstock/monolith/service/ShopService.java
new file mode 100644
index 0000000..b2d79a3
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/service/ShopService.java
@@ -0,0 +1,85 @@
+package de.mstock.monolith.service;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.mstock.monolith.domain.Category;
+import de.mstock.monolith.domain.CategoryI18n;
+import de.mstock.monolith.domain.CategoryRepository;
+import de.mstock.monolith.domain.Product;
+import de.mstock.monolith.domain.ProductI18n;
+import de.mstock.monolith.domain.ProductRepository;
+import de.mstock.monolith.web.CategoryDTO;
+import de.mstock.monolith.web.ProductDTO;
+
+@Service
+public class ShopService {
+
+ @Autowired
+ private CategoryRepository categoryRepository;
+
+ @Autowired
+ private ProductRepository productRepository;
+
+ /**
+ * Gets all categories of the current language.
+ *
+ * @return A simplified Data Transfer Object.
+ */
+ public List getCategories(Locale locale) {
+ String language = locale.getLanguage();
+ List categories = new ArrayList<>();
+ for (Category category : categoryRepository.findAllOrdered(language)) {
+ CategoryI18n i18n = category.getI18n().get(language);
+ categories.add(new CategoryDTO(i18n.getName(), i18n.getPrettyUrlFragment()));
+ }
+ return Collections.unmodifiableList(categories);
+ }
+
+ /**
+ * Gets all products for a category in the current language.
+ *
+ * @return A simplified Data Transfer Object.
+ */
+ public List getProductsForCategory(Locale locale, String prettyUrlFragment) {
+ String language = locale.getLanguage();
+ Category category = categoryRepository.findByPrettyUrlFragment(language, prettyUrlFragment);
+ if (category == null) {
+ throw new NotFoundException();
+ }
+ List products = new ArrayList<>();
+ NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
+ for (Product product : category.getProducts()) {
+ ProductI18n i18n = product.getI18n().get(language);
+ String price = currencyFormat.format(i18n.getPrice());
+ products.add(new ProductDTO(product.getItemNumber(), product.getUnit(), i18n.getName(),
+ i18n.getPrettyUrlFragment(), price, i18n.getDescription()));
+ }
+ return Collections.unmodifiableList(products);
+ }
+
+ /**
+ * Gets a products in the current language.
+ *
+ * @return A simplified Data Transfer Object.
+ */
+ public ProductDTO getProduct(Locale locale, String prettyUrlFragment) {
+ String language = locale.getLanguage();
+ Product product = productRepository.findByI18nName(language, prettyUrlFragment);
+ if (product == null) {
+ throw new NotFoundException();
+ }
+ ProductI18n i18n = product.getI18n().get(language);
+ NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale);
+ String price = currencyFormat.format(i18n.getPrice());
+ return new ProductDTO(product.getItemNumber(), product.getUnit(), i18n.getName(),
+ i18n.getPrettyUrlFragment(), price, i18n.getDescription());
+ }
+
+}
diff --git a/src/main/java/de/mstock/monolith/web/CategoryController.java b/src/main/java/de/mstock/monolith/web/CategoryController.java
index 3e05b26..045d859 100644
--- a/src/main/java/de/mstock/monolith/web/CategoryController.java
+++ b/src/main/java/de/mstock/monolith/web/CategoryController.java
@@ -1,20 +1,37 @@
package de.mstock.monolith.web;
+import java.util.Locale;
+
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
+import de.mstock.monolith.service.ShopService;
+
@Controller
public class CategoryController {
private static final String TEMPLATE = "category";
- @RequestMapping(value = "/categories/{name:[\\w-]+}", method = RequestMethod.GET)
- public String homepage(@PathVariable String name, Model model) {
- model.addAttribute("name", name);
- model.addAttribute("activeCategory", name);
+ @Autowired
+ private ShopService shopService;
+
+ /**
+ * Category page
+ *
+ * @param prettyUrlFragment Pretty URL fragment
+ * @param model Template model
+ * @param locale Current locale
+ * @return The template's name.
+ */
+ @RequestMapping(value = "/categories/{prettyUrlFragment:[\\w-]+}", method = RequestMethod.GET)
+ public String category(@PathVariable String prettyUrlFragment, Model model, Locale locale) {
+ model.addAttribute("categories", shopService.getCategories(locale));
+ model.addAttribute("products", shopService.getProductsForCategory(locale, prettyUrlFragment));
+ model.addAttribute("prettyUrlFragment", prettyUrlFragment);
return TEMPLATE;
}
}
diff --git a/src/main/java/de/mstock/monolith/web/CategoryDTO.java b/src/main/java/de/mstock/monolith/web/CategoryDTO.java
new file mode 100644
index 0000000..eccd3d1
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/web/CategoryDTO.java
@@ -0,0 +1,21 @@
+package de.mstock.monolith.web;
+
+public class CategoryDTO {
+
+ private final String name;
+ private final String prettyUrlFragment;
+
+ public CategoryDTO(String name, String prettyUrlFragment) {
+ this.name = name;
+ this.prettyUrlFragment = prettyUrlFragment;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getPrettyUrlFragment() {
+ return prettyUrlFragment;
+ }
+
+}
diff --git a/src/main/java/de/mstock/monolith/web/HomepageController.java b/src/main/java/de/mstock/monolith/web/HomepageController.java
index 9e48a99..90a0acc 100644
--- a/src/main/java/de/mstock/monolith/web/HomepageController.java
+++ b/src/main/java/de/mstock/monolith/web/HomepageController.java
@@ -1,17 +1,33 @@
package de.mstock.monolith.web;
+import java.util.Locale;
+
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
+import de.mstock.monolith.service.ShopService;
+
@Controller
public class HomepageController {
private static final String TEMPLATE = "homepage";
+ @Autowired
+ private ShopService shopService;
+
+ /**
+ * Homepage
+ *
+ * @param model Template model
+ * @param locale Current locale
+ * @return The template's name.
+ */
@RequestMapping(value = "/", method = RequestMethod.GET)
- public String homepage(Model model) {
+ public String homepage(Model model, Locale locale) {
+ model.addAttribute("categories", shopService.getCategories(locale));
return TEMPLATE;
}
}
diff --git a/src/main/java/de/mstock/monolith/web/ProductController.java b/src/main/java/de/mstock/monolith/web/ProductController.java
index 470a363..373f7f3 100644
--- a/src/main/java/de/mstock/monolith/web/ProductController.java
+++ b/src/main/java/de/mstock/monolith/web/ProductController.java
@@ -1,19 +1,36 @@
package de.mstock.monolith.web;
+import java.util.Locale;
+
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
+import de.mstock.monolith.service.ShopService;
+
@Controller
public class ProductController {
private static final String TEMPLATE = "product";
- @RequestMapping(value = "/products/{name:[\\w-]+}", method = RequestMethod.GET)
- public String homepage(@PathVariable String name, Model model) {
- model.addAttribute("name", name);
+ @Autowired
+ private ShopService shopService;
+
+ /**
+ * Product page
+ *
+ * @param prettyUrlFragment Pretty URL fragment
+ * @param model Template model
+ * @param locale Current locale
+ * @return The template's name.
+ */
+ @RequestMapping(value = "/products/{prettyUrlFragment:[\\w-]+}", method = RequestMethod.GET)
+ public String homepage(@PathVariable String prettyUrlFragment, Model model, Locale locale) {
+ model.addAttribute("categories", shopService.getCategories(locale));
+ model.addAttribute("product", shopService.getProduct(locale, prettyUrlFragment));
return TEMPLATE;
}
}
diff --git a/src/main/java/de/mstock/monolith/web/ProductDTO.java b/src/main/java/de/mstock/monolith/web/ProductDTO.java
new file mode 100644
index 0000000..e4a7302
--- /dev/null
+++ b/src/main/java/de/mstock/monolith/web/ProductDTO.java
@@ -0,0 +1,57 @@
+package de.mstock.monolith.web;
+
+import de.mstock.monolith.domain.ProductWeightUnit;
+
+public class ProductDTO {
+
+ private final String itemNumber;
+ private final ProductWeightUnit unit;
+ private final String name;
+ private final String prettyUrlFragment;
+ private final String price;
+ private final String description;
+
+ /**
+ * A simplified representation of a product.
+ *
+ * @param itemNumber A product's unique item number.
+ * @param unit The unit of a product that relates to its price.
+ * @param name The localized name.
+ * @param price The product's price per unit.
+ * @param description The description of the product.
+ */
+ public ProductDTO(String itemNumber, ProductWeightUnit unit, String name,
+ String prettyUrlFragment, String price, String description) {
+ this.itemNumber = itemNumber;
+ this.unit = unit;
+ this.name = name;
+ this.prettyUrlFragment = prettyUrlFragment;
+ this.price = price;
+ this.description = description;
+ }
+
+ public String getItemNumber() {
+ return itemNumber;
+ }
+
+ public ProductWeightUnit getUnit() {
+ return unit;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getPrice() {
+ return price;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getPrettyUrlFragment() {
+ return prettyUrlFragment;
+ }
+
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 0ec6399..05eb0f1 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -3,3 +3,6 @@ spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://${LAB_MONOLITH_DB_IP}:15432/monolith
spring.datasource.username=monolith
spring.datasource.password=${LAB_MONOLITH_DB_PASSWORD}
+
+spring.jpa.hibernate.ddl-auto=validate
+spring.jpa.show-sql=true
diff --git a/src/main/resources/db/migration/V1__create_initial_schema.sql b/src/main/resources/db/migration/V1__create_initial_schema.sql
new file mode 100644
index 0000000..7bc637b
--- /dev/null
+++ b/src/main/resources/db/migration/V1__create_initial_schema.sql
@@ -0,0 +1,37 @@
+create table categories (
+ id serial primary key,
+ ordinal integer not null
+);
+
+create table categories_i18n (
+ category_id integer,
+ locale_language varchar(2) not null,
+ name varchar(80) not null,
+ pretty_url_fragment varchar(31) not null,
+ primary key (category_id, locale_language)
+);
+
+create table products (
+ id serial primary key,
+ item_number varchar(8) not null,
+ unit integer not null
+);
+
+create table products_i18n (
+ product_id integer,
+ locale_language varchar(2) not null,
+ name varchar(80) not null,
+ pretty_url_fragment varchar(31) not null,
+ price numeric(10, 2) not null check (price > 0),
+ description varchar(1000),
+ primary key (product_id, locale_language)
+);
+
+create table category_products (
+ category_id integer references categories on delete cascade,
+ product_id integer references products on delete cascade,
+ ordinal integer,
+ primary key (category_id, product_id)
+);
+
+create unique index i_product_cascade on category_products(product_id, category_id);
diff --git a/src/main/resources/db/migration/V2__insert_data.sql b/src/main/resources/db/migration/V2__insert_data.sql
new file mode 100644
index 0000000..b476945
--- /dev/null
+++ b/src/main/resources/db/migration/V2__insert_data.sql
@@ -0,0 +1,41 @@
+insert into categories (id, ordinal) VALUES
+ (1, 10),
+ (2, 20);
+
+insert into categories_i18n (category_id, locale_language, name, pretty_url_fragment) values
+ (1, 'en', 'Fruits', 'fruits'),
+ (1, 'de', 'Obst', 'obst'),
+ (2, 'en', 'Vegetables', 'vegetables'),
+ (2, 'de', 'Gemüse', 'gemuese');
+
+insert into products (id, item_number, unit) values
+ (1, 'AS-62653', 1),
+ (2, 'KP-77763', 2),
+ (3, 'KL-58727', 2),
+ (4, 'RP-87973', 3),
+ (5, 'LC-52364', 2),
+ (6, 'WL-59573', 1);
+
+insert into products_i18n (product_id, locale_language, name, pretty_url_fragment, price, description) values
+ (1, 'en', 'Kiwis', 'kiwis', 0.21, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (1, 'de', 'Kiwis', 'kiwis', 0.19, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (2, 'en', 'Blueberries', 'blueberries', 2.68, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (2, 'de', 'Heidelbeeren', 'heidelbeeren', 2.52, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (3, 'en', 'Cherries', 'cherries', 1.67, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (3, 'de', 'Kirschen', 'kirschen', 1.57, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (4, 'en', 'Potatoes', 'potatoes', 1.75, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (4, 'de', 'Kartoffeln', 'kartoffeln', 1.65, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (5, 'en', 'Tomatoes', 'tomatoes', 1.18, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (5, 'de', 'Tomaten', 'tomaten', 1.11, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (6, 'en', 'Rhubarb', 'rhubarb', 1.52, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.'),
+ (6, 'de', 'Rhabarber', 'rhabarber', 1.43, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut.');
+
+insert into category_products (category_id, product_id, ordinal) values
+ (1, 1, null),
+ (1, 2, null),
+ (1, 3, 10),
+ (1, 5, null),
+ (1, 6, null),
+ (2, 4, 10),
+ (2, 5, 20),
+ (2, 6, null);
diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties
index 8390823..dc69cb4 100644
--- a/src/main/resources/messages.properties
+++ b/src/main/resources/messages.properties
@@ -2,12 +2,13 @@ company = Mosh
general.backToTop = Back to top
general.sampleImage = Sample image
navigation.toggle = Toggle navigation
-navigation.fruits = Fruits
-navigation.vegetables = Vegetables
navigation.legalNotice = Legal Notice
-products.kiwis = Kiwis
-products.blueberries = Blueberries
-products.cherries = Cherries
+products.kiwis=Kiwis
+products.blueberries=Blueberries
+products.cherries=Cherries
+products.kiwis.prettyUrlFragment=kiwis
+products.blueberries.prettyUrlFragment=blueberries
+products.cherries.prettyUrlFragment=cherries
products.goto = Go to product
features.headline1 = Delicious Fruits.
features.subheadline1 = It'll blow your mind.
diff --git a/src/main/resources/messages_de.properties b/src/main/resources/messages_de.properties
index c811366..b19516f 100644
--- a/src/main/resources/messages_de.properties
+++ b/src/main/resources/messages_de.properties
@@ -1,12 +1,13 @@
general.backToTop = Zurück nach oben
general.sampleImage = Beispielfoto
navigation.toggle = Navigation umschalten
-navigation.fruits = Obst
-navigation.vegetables = Gemüse
navigation.legalNotice = Impressum
-products.kiwis = Kiwis
-products.blueberries = Heidelbeeren
-products.cherries = Kirschen
+products.kiwis=Kiwis
+products.blueberries=Blaubeeren
+products.cherries=Kirschen
+products.kiwis.prettyUrlFragment=kiwis
+products.blueberries.prettyUrlFragment=heidelbeeren
+products.cherries.prettyUrlFragment=kirschen
products.goto = Zum Produkt
features.headline1 = Frisches Obst.
features.subheadline1 = Es wird dich umhauen.
diff --git a/src/main/resources/templates/category.html b/src/main/resources/templates/category.html
index 185e42d..db9e1df 100644
--- a/src/main/resources/templates/category.html
+++ b/src/main/resources/templates/category.html
@@ -17,8 +17,8 @@
-
-
-
-
- [[#{general.sampleImage}]]: Rach. New Packaging. CC BY 2.0
-
-
-
-
0,00 Euro
-
-
-
-
-
- [[#{general.sampleImage}]]: Rach. New Packaging. CC BY 2.0
-
-
-
-
0,00 Euro
-
-
-
-
-
- [[#{general.sampleImage}]]: Rach. New Packaging. CC BY 2.0
-
-
-
-
0,00 Euro
-
-
-
-
-
-
-
-
- [[#{general.sampleImage}]]: Rach. New Packaging. CC BY 2.0
-
-
-
-
0,00 Euro
-
-
-
-
-
- [[#{general.sampleImage}]]: Rach. New Packaging. CC BY 2.0
-
-
-
-
0,00 Euro
-
-
-
-
-
- [[#{general.sampleImage}]]: Rach. New Packaging. CC BY 2.0
-
-
-
-
0,00 Euro
+
1,47 Euro
diff --git a/src/main/resources/templates/fragments/skeleton.html b/src/main/resources/templates/fragments/skeleton.html
index 6ead40e..9282eb1 100644
--- a/src/main/resources/templates/fragments/skeleton.html
+++ b/src/main/resources/templates/fragments/skeleton.html
@@ -36,12 +36,9 @@
diff --git a/src/main/resources/templates/homepage.html b/src/main/resources/templates/homepage.html
index a8928ca..00f021d 100644
--- a/src/main/resources/templates/homepage.html
+++ b/src/main/resources/templates/homepage.html
@@ -109,7 +109,7 @@
class="text-emphasized">Kiwi. CC BY 2.0
- Go to product
@@ -128,7 +128,7 @@
class="text-emphasized">Blueberries. CC BY 2.0
- Go to product
@@ -148,7 +148,7 @@
class="text-emphasized">Cherries. CC BY 2.0
- Go to product
diff --git a/src/main/resources/templates/product.html b/src/main/resources/templates/product.html
index 0b4d509..0d7bc31 100644
--- a/src/main/resources/templates/product.html
+++ b/src/main/resources/templates/product.html
@@ -30,9 +30,9 @@
-
Product Name
-
0,00 Euro
-
Description.
+
Product Name
+
0,00 Euro
+
Description.
diff --git a/src/test/java/de/mstock/MonolithApplicationTests.java b/src/test/java/de/mstock/monolith/MonolithApplicationTests.java
similarity index 68%
rename from src/test/java/de/mstock/MonolithApplicationTests.java
rename to src/test/java/de/mstock/monolith/MonolithApplicationTests.java
index c5fee0e..57e04c8 100644
--- a/src/test/java/de/mstock/MonolithApplicationTests.java
+++ b/src/test/java/de/mstock/monolith/MonolithApplicationTests.java
@@ -1,4 +1,6 @@
-package de.mstock;
+package de.mstock.monolith;
+
+import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -10,6 +12,8 @@ import org.springframework.test.context.junit4.SpringRunner;
public class MonolithApplicationTests {
@Test
- public void contextLoads() {}
+ public void contextLoads() {
+ assertTrue(true);
+ }
}
diff --git a/src/test/java/de/mstock/monolith/service/ShopServiceTest.java b/src/test/java/de/mstock/monolith/service/ShopServiceTest.java
new file mode 100644
index 0000000..92cba32
--- /dev/null
+++ b/src/test/java/de/mstock/monolith/service/ShopServiceTest.java
@@ -0,0 +1,64 @@
+package de.mstock.monolith.service;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import de.mstock.monolith.domain.Category;
+import de.mstock.monolith.domain.CategoryRepository;
+import de.mstock.monolith.domain.Product;
+import de.mstock.monolith.domain.ProductRepository;
+import de.mstock.monolith.web.CategoryDTO;
+import de.mstock.monolith.web.ProductDTO;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ShopServiceTest {
+
+ @Mock
+ private CategoryRepository categoryRepository;
+
+ @Mock
+ private ProductRepository productRepository;
+
+ @InjectMocks
+ private ShopService shopService;
+
+ @Test
+ public void shouldGetDataTransferObjectForEveryEntity() {
+ Locale locale = new Locale("de");
+ Category category = mock(Category.class, RETURNS_DEEP_STUBS);
+ List categoryEntities = Arrays.asList(category, category, category);
+ when(categoryRepository.findAllOrdered(eq(locale.getLanguage()))).thenReturn(categoryEntities);
+ List categories = shopService.getCategories(locale);
+ assertThat("Same amount of categories", categories.size(),
+ is(equalTo(categoryEntities.size())));
+ }
+
+ @Test
+ public void shouldFormatPrice() {
+ Locale locale = new Locale("en", "US");
+ Product product = mock(Product.class, RETURNS_DEEP_STUBS);
+ when(product.getI18n().get(anyString()).getPrice()).thenReturn(BigDecimal.valueOf(1.47));
+ when(productRepository.findByI18nName(eq(locale.getLanguage()), anyString()))
+ .thenReturn(product);
+ ProductDTO productDTO = shopService.getProduct(locale, "foo");
+ assertThat("Product has a formatted price", productDTO.getPrice(), is(equalTo("$1.47")));
+ }
+
+}