Products From Database
- Categories - Products - i18n - Templating
This commit is contained in:
parent
6bbc843ba0
commit
27cfc8a476
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -159,12 +159,12 @@
|
|||
</module>
|
||||
<module name="AbbreviationAsWordInName">
|
||||
<property name="ignoreFinal" value="false"/>
|
||||
<property name="allowedAbbreviationLength" value="1"/>
|
||||
<property name="allowedAbbreviationLength" value="3"/>
|
||||
</module>
|
||||
<module name="OverloadMethodsDeclarationOrder"/>
|
||||
<module name="VariableDeclarationUsageDistance"/>
|
||||
<module name="CustomImportOrder">
|
||||
<property name="sortImportsInGroupAlphabetically" value="true"/>
|
||||
<!-- <property name="sortImportsInGroupAlphabetically" value="true"/> -->
|
||||
<property name="separateLineBetweenGroups" value="true"/>
|
||||
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
|
||||
</module>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Locale> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, CategoryI18n> i18n;
|
||||
@ManyToMany
|
||||
@JoinTable(name = "category_products", inverseJoinColumns = @JoinColumn(name = "product_id"))
|
||||
private List<Product> 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<String, CategoryI18n> getI18n() {
|
||||
return i18n;
|
||||
}
|
||||
|
||||
public void setI18n(Map<String, CategoryI18n> i18n) {
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
public List<Product> getProducts() {
|
||||
return products;
|
||||
}
|
||||
|
||||
public void setProducts(List<Product> products) {
|
||||
this.products = products;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Category, Integer> {
|
||||
|
||||
@Query("select distinct c from Category c join fetch c.i18n i "
|
||||
+ "where key(i) = ?1 order by c.ordinal")
|
||||
List<Category> 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);
|
||||
|
||||
}
|
|
@ -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<String, ProductI18n> 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<String, ProductI18n> getI18n() {
|
||||
return i18n;
|
||||
}
|
||||
|
||||
public void setI18n(Map<String, ProductI18n> i18n) {
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Product, Integer> {
|
||||
|
||||
@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);
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package de.mstock.monolith.domain;
|
||||
|
||||
public enum ProductWeightUnit {
|
||||
UNKNOWN, PIECE, GRAMM_100, KILO_1
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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<CategoryDTO> getCategories(Locale locale) {
|
||||
String language = locale.getLanguage();
|
||||
List<CategoryDTO> 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<ProductDTO> getProductsForCategory(Locale locale, String prettyUrlFragment) {
|
||||
String language = locale.getLanguage();
|
||||
Category category = categoryRepository.findByPrettyUrlFragment(language, prettyUrlFragment);
|
||||
if (category == null) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
List<ProductDTO> 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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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.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.
|
||||
|
|
|
@ -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.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.
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
<div class="container category">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<a href="#"><img
|
||||
<div class="col-md-3" th:each="product, row : ${products}">
|
||||
<a href="#" th:href="@{/products/__${product.prettyUrlFragment}__.html}"><img
|
||||
class="featurette-image img-responsive center-block"
|
||||
src="../static/images/rach-new_packaging-cc_by_2_0.jpg"
|
||||
th:src="@{/images/rach-new_packaging-cc_by_2_0.jpg}"
|
||||
|
@ -30,114 +30,9 @@
|
|||
</small>
|
||||
</div>
|
||||
<h5>
|
||||
<a href="#">Product</a>
|
||||
<a href="#" th:text="${product.name}" th:href="@{/products/__${product.prettyUrlFragment}__.html}">Product</a>
|
||||
</h5>
|
||||
<p>0,00 Euro</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="#"><img
|
||||
class="featurette-image img-responsive center-block"
|
||||
src="../static/images/rach-new_packaging-cc_by_2_0.jpg"
|
||||
th:src="@{/images/rach-new_packaging-cc_by_2_0.jpg}"
|
||||
alt="Generic placeholder image" /></a>
|
||||
<div>
|
||||
<small class="text-muted" th:inline="text">
|
||||
[[#{general.sampleImage}]]: Rach. <span
|
||||
class="text-emphasized">New Packaging.</span> CC BY 2.0
|
||||
</small>
|
||||
</div>
|
||||
<h5>
|
||||
<a href="#">Product</a>
|
||||
</h5>
|
||||
<p>0,00 Euro</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="#"><img
|
||||
class="featurette-image img-responsive center-block"
|
||||
src="../static/images/rach-new_packaging-cc_by_2_0.jpg"
|
||||
th:src="@{/images/rach-new_packaging-cc_by_2_0.jpg}"
|
||||
alt="Generic placeholder image" /></a>
|
||||
<div>
|
||||
<small class="text-muted" th:inline="text">
|
||||
[[#{general.sampleImage}]]: Rach. <span
|
||||
class="text-emphasized">New Packaging.</span> CC BY 2.0
|
||||
</small>
|
||||
</div>
|
||||
<h5>
|
||||
<a href="#">Product</a>
|
||||
</h5>
|
||||
<p>0,00 Euro</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="#"><img
|
||||
class="featurette-image img-responsive center-block"
|
||||
src="../static/images/rach-new_packaging-cc_by_2_0.jpg"
|
||||
th:src="@{/images/rach-new_packaging-cc_by_2_0.jpg}"
|
||||
alt="Generic placeholder image" /></a>
|
||||
<div>
|
||||
<small class="text-muted" th:inline="text">
|
||||
[[#{general.sampleImage}]]: Rach. <span
|
||||
class="text-emphasized">New Packaging.</span> CC BY 2.0
|
||||
</small>
|
||||
</div>
|
||||
<h5>
|
||||
<a href="#">Product</a>
|
||||
</h5>
|
||||
<p>0,00 Euro</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<a href="#"><img
|
||||
class="featurette-image img-responsive center-block"
|
||||
src="../static/images/rach-new_packaging-cc_by_2_0.jpg"
|
||||
th:src="@{/images/rach-new_packaging-cc_by_2_0.jpg}"
|
||||
alt="Generic placeholder image" /></a>
|
||||
<div>
|
||||
<small class="text-muted" th:inline="text">
|
||||
[[#{general.sampleImage}]]: Rach. <span
|
||||
class="text-emphasized">New Packaging.</span> CC BY 2.0
|
||||
</small>
|
||||
</div>
|
||||
<h5>
|
||||
<a href="#">Product</a>
|
||||
</h5>
|
||||
<p>0,00 Euro</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="#"><img
|
||||
class="featurette-image img-responsive center-block"
|
||||
src="../static/images/rach-new_packaging-cc_by_2_0.jpg"
|
||||
th:src="@{/images/rach-new_packaging-cc_by_2_0.jpg}"
|
||||
alt="Generic placeholder image" /></a>
|
||||
<div>
|
||||
<small class="text-muted" th:inline="text">
|
||||
[[#{general.sampleImage}]]: Rach. <span
|
||||
class="text-emphasized">New Packaging.</span> CC BY 2.0
|
||||
</small>
|
||||
</div>
|
||||
<h5>
|
||||
<a href="#">Product</a>
|
||||
</h5>
|
||||
<p>0,00 Euro</p>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="#"><img
|
||||
class="featurette-image img-responsive center-block"
|
||||
src="../static/images/rach-new_packaging-cc_by_2_0.jpg"
|
||||
th:src="@{/images/rach-new_packaging-cc_by_2_0.jpg}"
|
||||
alt="Generic placeholder image" /></a>
|
||||
<div>
|
||||
<small class="text-muted" th:inline="text">
|
||||
[[#{general.sampleImage}]]: Rach. <span
|
||||
class="text-emphasized">New Packaging.</span> CC BY 2.0
|
||||
</small>
|
||||
</div>
|
||||
<h5>
|
||||
<a href="#">Product</a>
|
||||
</h5>
|
||||
<p>0,00 Euro</p>
|
||||
<p th:text="${product.price}">1,47 Euro</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -36,12 +36,9 @@
|
|||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li th:class="${activeCategory == 'fruits'} ? 'active'"><a href="#"
|
||||
th:href="@{/categories/fruits.html}"
|
||||
th:text="#{navigation.fruits}">Fruits</a></li>
|
||||
<li th:class="${activeCategory == 'vegetables'} ? 'active'"><a href="#"
|
||||
th:href="@{/categories/vegetables.html}"
|
||||
th:text="#{navigation.vegetables}">Vegetables</a></li>
|
||||
<li th:each="category : ${categories}" th:class="${category.prettyUrlFragment == prettyUrlFragment} ? 'active'"><a href="#"
|
||||
th:href="@{/categories/__${category.prettyUrlFragment}__.html}"
|
||||
th:text="${category.name}">Category</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
class="text-emphasized">Kiwi.</span> CC BY 2.0
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn btn-default" href="products/kiwis.html"
|
||||
<a class="btn btn-default" href="#" th:href="@{/products/__#{products.kiwis.prettyUrlFragment}__.html}"
|
||||
role="button" th:text="#{products.goto}">Go to product</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -128,7 +128,7 @@
|
|||
class="text-emphasized">Blueberries.</span> CC BY 2.0
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn btn-default" href="products/blueberries.html"
|
||||
<a class="btn btn-default" href="#" th:href="@{/products/__#{products.blueberries.prettyUrlFragment}__.html}"
|
||||
role="button" th:text="#{products.goto}">Go to product</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -148,7 +148,7 @@
|
|||
class="text-emphasized">Cherries.</span> CC BY 2.0
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn btn-default" href="products/cherries.html"
|
||||
<a class="btn btn-default" href="#" th:href="@{/products/__#{products.cherries.prettyUrlFragment}__.html}"
|
||||
role="button" th:text="#{products.goto}">Go to product</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -30,9 +30,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<h2 th:text="${name}">Product Name</h2>
|
||||
<p class="text-info text-uppercase">0,00 Euro</p>
|
||||
<p class="lead">Description.</p>
|
||||
<h2 th:text="${product.name}">Product Name</h2>
|
||||
<p class="text-info text-uppercase" th:text="${product.price}">0,00 Euro</p>
|
||||
<p class="lead" th:text="${product.description}">Description.</p>
|
||||
<div th:replace="fragments/reviews :: reviews"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Category> categoryEntities = Arrays.asList(category, category, category);
|
||||
when(categoryRepository.findAllOrdered(eq(locale.getLanguage()))).thenReturn(categoryEntities);
|
||||
List<CategoryDTO> 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")));
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue