genofire/hs_monolith
genofire
/
hs_monolith
Archived
1
0
Fork 0

Products From Database

- Categories
- Products
- i18n
- Templating
This commit is contained in:
Matthias Stock 2017-03-08 23:41:35 +01:00
parent 6bbc843ba0
commit 27cfc8a476
31 changed files with 802 additions and 160 deletions

View File

@ -50,7 +50,10 @@ dependencies {
compile('org.springframework.boot:spring-boot-starter-security') compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-thymeleaf') compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.springframework.boot:spring-boot-starter-web') 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.springframework.boot:spring-boot-devtools')
runtime('org.postgresql:postgresql') runtime('org.postgresql:postgresql')
testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.mockito:mockito-core:2.7.14')
testCompile('org.hamcrest:hamcrest-core:1.3')
} }

View File

@ -159,12 +159,12 @@
</module> </module>
<module name="AbbreviationAsWordInName"> <module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/> <property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/> <property name="allowedAbbreviationLength" value="3"/>
</module> </module>
<module name="OverloadMethodsDeclarationOrder"/> <module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance"/> <module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder"> <module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/> <!-- <property name="sortImportsInGroupAlphabetically" value="true"/> -->
<property name="separateLineBetweenGroups" value="true"/> <property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/> <property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
</module> </module>

View File

@ -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();
}
}

View File

@ -1,34 +1,32 @@
package de.mstock.monolith.config; package de.mstock.monolith.config;
import java.util.Arrays;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver; 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.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
@Configuration @Configuration
public class I18nConfig extends WebMvcConfigurerAdapter { 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 @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
slr.setDefaultLocale(Locale.US); localeResolver.setSupportedLocales(SUPPORTED_LOCALES);
return slr; 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());
}
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -0,0 +1,5 @@
package de.mstock.monolith.domain;
public enum ProductWeightUnit {
UNKNOWN, PIECE, GRAMM_100, KILO_1
}

View File

@ -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;
}

View File

@ -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());
}
}

View File

@ -1,20 +1,37 @@
package de.mstock.monolith.web; package de.mstock.monolith.web;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import de.mstock.monolith.service.ShopService;
@Controller @Controller
public class CategoryController { public class CategoryController {
private static final String TEMPLATE = "category"; private static final String TEMPLATE = "category";
@RequestMapping(value = "/categories/{name:[\\w-]+}", method = RequestMethod.GET) @Autowired
public String homepage(@PathVariable String name, Model model) { private ShopService shopService;
model.addAttribute("name", name);
model.addAttribute("activeCategory", name); /**
* 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; return TEMPLATE;
} }
} }

View File

@ -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;
}
}

View File

@ -1,17 +1,33 @@
package de.mstock.monolith.web; package de.mstock.monolith.web;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import de.mstock.monolith.service.ShopService;
@Controller @Controller
public class HomepageController { public class HomepageController {
private static final String TEMPLATE = "homepage"; 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) @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; return TEMPLATE;
} }
} }

View File

@ -1,19 +1,36 @@
package de.mstock.monolith.web; package de.mstock.monolith.web;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import de.mstock.monolith.service.ShopService;
@Controller @Controller
public class ProductController { public class ProductController {
private static final String TEMPLATE = "product"; private static final String TEMPLATE = "product";
@RequestMapping(value = "/products/{name:[\\w-]+}", method = RequestMethod.GET) @Autowired
public String homepage(@PathVariable String name, Model model) { private ShopService shopService;
model.addAttribute("name", name);
/**
* 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; return TEMPLATE;
} }
} }

View File

@ -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;
}
}

View File

@ -3,3 +3,6 @@ spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://${LAB_MONOLITH_DB_IP}:15432/monolith spring.datasource.url=jdbc:postgresql://${LAB_MONOLITH_DB_IP}:15432/monolith
spring.datasource.username=monolith spring.datasource.username=monolith
spring.datasource.password=${LAB_MONOLITH_DB_PASSWORD} spring.datasource.password=${LAB_MONOLITH_DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true

View File

@ -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);

View File

@ -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);

View File

@ -2,12 +2,13 @@ company = Mosh
general.backToTop = Back to top general.backToTop = Back to top
general.sampleImage = Sample image general.sampleImage = Sample image
navigation.toggle = Toggle navigation navigation.toggle = Toggle navigation
navigation.fruits = Fruits
navigation.vegetables = Vegetables
navigation.legalNotice = Legal Notice navigation.legalNotice = Legal Notice
products.kiwis = Kiwis products.kiwis=Kiwis
products.blueberries = Blueberries products.blueberries=Blueberries
products.cherries = Cherries products.cherries=Cherries
products.kiwis.prettyUrlFragment=kiwis
products.blueberries.prettyUrlFragment=blueberries
products.cherries.prettyUrlFragment=cherries
products.goto = Go to product products.goto = Go to product
features.headline1 = Delicious Fruits. features.headline1 = Delicious Fruits.
features.subheadline1 = It'll blow your mind. features.subheadline1 = It'll blow your mind.

View File

@ -1,12 +1,13 @@
general.backToTop = Zurück nach oben general.backToTop = Zurück nach oben
general.sampleImage = Beispielfoto general.sampleImage = Beispielfoto
navigation.toggle = Navigation umschalten navigation.toggle = Navigation umschalten
navigation.fruits = Obst
navigation.vegetables = Gemüse
navigation.legalNotice = Impressum navigation.legalNotice = Impressum
products.kiwis = Kiwis products.kiwis=Kiwis
products.blueberries = Heidelbeeren products.blueberries=Blaubeeren
products.cherries = Kirschen products.cherries=Kirschen
products.kiwis.prettyUrlFragment=kiwis
products.blueberries.prettyUrlFragment=heidelbeeren
products.cherries.prettyUrlFragment=kirschen
products.goto = Zum Produkt products.goto = Zum Produkt
features.headline1 = Frisches Obst. features.headline1 = Frisches Obst.
features.subheadline1 = Es wird dich umhauen. features.subheadline1 = Es wird dich umhauen.

View File

@ -17,8 +17,8 @@
<div class="container category"> <div class="container category">
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3" th:each="product, row : ${products}">
<a href="#"><img <a href="#" th:href="@{/products/__${product.prettyUrlFragment}__.html}"><img
class="featurette-image img-responsive center-block" class="featurette-image img-responsive center-block"
src="../static/images/rach-new_packaging-cc_by_2_0.jpg" src="../static/images/rach-new_packaging-cc_by_2_0.jpg"
th:src="@{/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> </small>
</div> </div>
<h5> <h5>
<a href="#">Product</a> <a href="#" th:text="${product.name}" th:href="@{/products/__${product.prettyUrlFragment}__.html}">Product</a>
</h5> </h5>
<p>0,00 Euro</p> <p th:text="${product.price}">1,47 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>
</div> </div>
</div> </div>

View File

@ -36,12 +36,9 @@
</div> </div>
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li th:class="${activeCategory == 'fruits'} ? 'active'"><a href="#" <li th:each="category : ${categories}" th:class="${category.prettyUrlFragment == prettyUrlFragment} ? 'active'"><a href="#"
th:href="@{/categories/fruits.html}" th:href="@{/categories/__${category.prettyUrlFragment}__.html}"
th:text="#{navigation.fruits}">Fruits</a></li> th:text="${category.name}">Category</a></li>
<li th:class="${activeCategory == 'vegetables'} ? 'active'"><a href="#"
th:href="@{/categories/vegetables.html}"
th:text="#{navigation.vegetables}">Vegetables</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -109,7 +109,7 @@
class="text-emphasized">Kiwi.</span> CC BY 2.0 class="text-emphasized">Kiwi.</span> CC BY 2.0
</p> </p>
<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> role="button" th:text="#{products.goto}">Go to product</a>
</p> </p>
</div> </div>
@ -128,7 +128,7 @@
class="text-emphasized">Blueberries.</span> CC BY 2.0 class="text-emphasized">Blueberries.</span> CC BY 2.0
</p> </p>
<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> role="button" th:text="#{products.goto}">Go to product</a>
</p> </p>
</div> </div>
@ -148,7 +148,7 @@
class="text-emphasized">Cherries.</span> CC BY 2.0 class="text-emphasized">Cherries.</span> CC BY 2.0
</p> </p>
<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> role="button" th:text="#{products.goto}">Go to product</a>
</p> </p>
</div> </div>

View File

@ -30,9 +30,9 @@
</div> </div>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<h2 th:text="${name}">Product Name</h2> <h2 th:text="${product.name}">Product Name</h2>
<p class="text-info text-uppercase">0,00 Euro</p> <p class="text-info text-uppercase" th:text="${product.price}">0,00 Euro</p>
<p class="lead">Description.</p> <p class="lead" th:text="${product.description}">Description.</p>
<div th:replace="fragments/reviews :: reviews"></div> <div th:replace="fragments/reviews :: reviews"></div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,6 @@
package de.mstock; package de.mstock.monolith;
import static org.junit.Assert.assertTrue;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -10,6 +12,8 @@ import org.springframework.test.context.junit4.SpringRunner;
public class MonolithApplicationTests { public class MonolithApplicationTests {
@Test @Test
public void contextLoads() {} public void contextLoads() {
assertTrue(true);
}
} }

View File

@ -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")));
}
}