diff --git a/src/main/java/de/mstock/monolith/domain/Category.java b/src/main/java/de/mstock/monolith/domain/Category.java index b7e1abf..202cd6b 100644 --- a/src/main/java/de/mstock/monolith/domain/Category.java +++ b/src/main/java/de/mstock/monolith/domain/Category.java @@ -4,6 +4,8 @@ import java.util.List; import java.util.Map; import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; @@ -17,6 +19,7 @@ import javax.persistence.Table; public class Category { @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private int ordinal; @OneToMany(mappedBy = "category") diff --git a/src/main/java/de/mstock/monolith/domain/DataTransferObjectFactory.java b/src/main/java/de/mstock/monolith/domain/DataTransferObjectFactory.java new file mode 100644 index 0000000..914d1ae --- /dev/null +++ b/src/main/java/de/mstock/monolith/domain/DataTransferObjectFactory.java @@ -0,0 +1,122 @@ +package de.mstock.monolith.domain; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.springframework.stereotype.Component; + +import de.mstock.monolith.web.CategoryDTO; +import de.mstock.monolith.web.ProductDTO; +import de.mstock.monolith.web.ReviewDTO; + +@Component +public class DataTransferObjectFactory { + + + /** + * Creates a Data Transfer Object (DTO). + * + * @param category database entity + * @param locale the requested locale + * @return DTO + */ + public CategoryDTO createCategoryDTO(Category category, Locale locale) { + CategoryI18n i18n = category.getI18n().get(locale.getLanguage()); + CategoryDTO categoryDTO = new CategoryDTO(); + categoryDTO.setName(i18n.getName()); + categoryDTO.setPrettyUrlFragment(i18n.getPrettyUrlFragment()); + return categoryDTO; + } + + /** + * Creates a Data Transfer Object (DTO). + * + * @param product database entity + * @param locale the requested locale + * @return DTO + */ + public ProductDTO createProductDTO(Product product, Locale locale) { + return createProductDTO(product, locale, NumberFormat.getCurrencyInstance(locale)); + } + + private ProductDTO createProductDTO(Product product, Locale locale, NumberFormat numberFormat) { + ProductDTO productDTO = createProductWithoutReviewsDTO(product, locale, numberFormat); + ProductI18n i18n = product.getI18n().get(locale.getLanguage()); + productDTO.setReviews(createReviewDTOs(i18n.getReviews())); + return productDTO; + } + + /** + * Creates Data Transfer Objects (DTOs). + * + * @param products database entities + * @param locale the requested locale + * @return DTOs + */ + public List createProductDTOs(List products, Locale locale) { + List productDTOs = new ArrayList<>(products.size()); + NumberFormat numberFormat = NumberFormat.getCurrencyInstance(locale); + for (Product product : products) { + productDTOs.add(createProductDTO(product, locale, numberFormat)); + } + return productDTOs; + } + + /** + * Creates Data Transfer Objects (DTOs) without loading their reviews. + * + * @param products database entities + * @param locale the requested locale + * @return DTOs + */ + public List createProductWithoutReviewsDTOs(List products, Locale locale) { + List productDTOs = new ArrayList<>(products.size()); + NumberFormat numberFormat = NumberFormat.getCurrencyInstance(locale); + for (Product product : products) { + productDTOs.add(createProductWithoutReviewsDTO(product, locale, numberFormat)); + } + return productDTOs; + } + + private ProductDTO createProductWithoutReviewsDTO(Product product, Locale locale, + NumberFormat numberFormat) { + ProductI18n i18n = product.getI18n().get(locale.getLanguage()); + String price = numberFormat.format(i18n.getPrice()); + ProductDTO productDTO = new ProductDTO(); + productDTO.setItemNumber(product.getItemNumber()); + productDTO.setUnit(product.getUnit()); + productDTO.setName(i18n.getName()); + productDTO.setPrettyUrlFragment(i18n.getPrettyUrlFragment()); + productDTO.setPrice(price); + productDTO.setDescription(i18n.getDescription()); + return productDTO; + } + + /** + * Creates a Data Transfer Object (DTO). + * + * @param review database entity + * @return DTO + */ + public ReviewDTO createReviewDTO(Review review) { + ReviewDTO dto = new ReviewDTO(); + dto.setLanguage(review.getLocaleLanguage()); + dto.setRatingStars(review.getRatingStars()); + dto.setFirstName(review.getFirstName()); + dto.setLastName(review.getLastName()); + dto.setText(review.getText()); + return dto; + } + + private List createReviewDTOs(List reviews) { + List ratingDTOs = new ArrayList<>(reviews.size()); + for (Review review : reviews) { + ratingDTOs.add(createReviewDTO(review)); + } + return Collections.unmodifiableList(ratingDTOs); + } + +} diff --git a/src/main/java/de/mstock/monolith/domain/Product.java b/src/main/java/de/mstock/monolith/domain/Product.java index 52e5131..c12264d 100644 --- a/src/main/java/de/mstock/monolith/domain/Product.java +++ b/src/main/java/de/mstock/monolith/domain/Product.java @@ -5,6 +5,8 @@ import java.util.Map; import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.MapKey; import javax.persistence.OneToMany; @@ -15,6 +17,7 @@ import javax.persistence.Table; public class Product { @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String itemNumber; @Enumerated(EnumType.ORDINAL) diff --git a/src/main/java/de/mstock/monolith/domain/ProductI18n.java b/src/main/java/de/mstock/monolith/domain/ProductI18n.java index 3ccc205..808764c 100644 --- a/src/main/java/de/mstock/monolith/domain/ProductI18n.java +++ b/src/main/java/de/mstock/monolith/domain/ProductI18n.java @@ -1,11 +1,15 @@ package de.mstock.monolith.domain; import java.math.BigDecimal; +import java.util.List; import javax.persistence.EmbeddedId; import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.JoinColumns; import javax.persistence.ManyToOne; import javax.persistence.MapsId; +import javax.persistence.OneToMany; import javax.persistence.Table; @Entity @@ -21,6 +25,9 @@ public class ProductI18n { private String prettyUrlFragment; private BigDecimal price; private String description; + @OneToMany + @JoinColumns({@JoinColumn(name = "localeLanguage"), @JoinColumn(name = "productId")}) + private List reviews; public ProductI18nPk getProductI18nPk() { return productI18nPk; @@ -61,4 +68,12 @@ public class ProductI18n { public void setDescription(String description) { this.description = description; } + + public List getReviews() { + return reviews; + } + + public void setReviews(List reviews) { + this.reviews = reviews; + } } diff --git a/src/main/java/de/mstock/monolith/domain/ProductRepository.java b/src/main/java/de/mstock/monolith/domain/ProductRepository.java index b0259e0..e811855 100644 --- a/src/main/java/de/mstock/monolith/domain/ProductRepository.java +++ b/src/main/java/de/mstock/monolith/domain/ProductRepository.java @@ -5,7 +5,7 @@ import org.springframework.data.repository.Repository; public interface ProductRepository extends Repository { - @Query("select p from Product p join fetch p.i18n i " + @Query("select p from Product p join fetch p.i18n i left join fetch i.reviews r " + "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/Review.java b/src/main/java/de/mstock/monolith/domain/Review.java new file mode 100644 index 0000000..c6306df --- /dev/null +++ b/src/main/java/de/mstock/monolith/domain/Review.java @@ -0,0 +1,79 @@ +package de.mstock.monolith.domain; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "reviews") +public class Review { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + private int productId; + private String localeLanguage; + private String firstName; + private String lastName; + private int ratingStars; + private String text; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + 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; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getRatingStars() { + return ratingStars; + } + + public void setRatingStars(int ratingStars) { + this.ratingStars = ratingStars; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + +} diff --git a/src/main/java/de/mstock/monolith/domain/ReviewRepository.java b/src/main/java/de/mstock/monolith/domain/ReviewRepository.java new file mode 100644 index 0000000..361604f --- /dev/null +++ b/src/main/java/de/mstock/monolith/domain/ReviewRepository.java @@ -0,0 +1,7 @@ +package de.mstock.monolith.domain; + +import org.springframework.data.repository.CrudRepository; + +public interface ReviewRepository extends CrudRepository { + +} diff --git a/src/main/java/de/mstock/monolith/service/ReviewService.java b/src/main/java/de/mstock/monolith/service/ReviewService.java new file mode 100644 index 0000000..73f60da --- /dev/null +++ b/src/main/java/de/mstock/monolith/service/ReviewService.java @@ -0,0 +1,47 @@ +package de.mstock.monolith.service; + +import java.util.Locale; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import de.mstock.monolith.domain.DataTransferObjectFactory; +import de.mstock.monolith.domain.Product; +import de.mstock.monolith.domain.ProductRepository; +import de.mstock.monolith.domain.Review; +import de.mstock.monolith.domain.ReviewRepository; +import de.mstock.monolith.web.ReviewDTO; +import de.mstock.monolith.web.form.ReviewForm; + +@Service +public class ReviewService { + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private DataTransferObjectFactory dtoFactory; + + /** + * Stores a review from a posted form. + * + * @param reviewForm Post data + * @param locale Language context + * @param prettyUrlFragment Used to get the product context + * @return DTO + */ + public ReviewDTO saveReview(ReviewForm reviewForm, Locale locale, String prettyUrlFragment) { + Product product = productRepository.findByI18nName(locale.getLanguage(), prettyUrlFragment); + Review review = new Review(); + review.setProductId(product.getId()); + review.setLocaleLanguage(locale.getLanguage()); + review.setFirstName(reviewForm.getFirstName()); + review.setLastName(reviewForm.getLastName()); + review.setRatingStars(reviewForm.getRatingStars()); + review.setText(reviewForm.getText()); + return dtoFactory.createReviewDTO(reviewRepository.save(review)); + } +} diff --git a/src/main/java/de/mstock/monolith/service/ShopService.java b/src/main/java/de/mstock/monolith/service/ShopService.java index b2d79a3..c230e45 100644 --- a/src/main/java/de/mstock/monolith/service/ShopService.java +++ b/src/main/java/de/mstock/monolith/service/ShopService.java @@ -1,6 +1,5 @@ package de.mstock.monolith.service; -import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -10,10 +9,9 @@ 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.DataTransferObjectFactory; 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; @@ -27,6 +25,9 @@ public class ShopService { @Autowired private ProductRepository productRepository; + @Autowired + private DataTransferObjectFactory dtoFactory; + /** * Gets all categories of the current language. * @@ -36,8 +37,7 @@ public class ShopService { 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())); + categories.add(dtoFactory.createCategoryDTO(category, locale)); } return Collections.unmodifiableList(categories); } @@ -53,33 +53,22 @@ public class ShopService { 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())); - } + List products = + dtoFactory.createProductWithoutReviewsDTOs(category.getProducts(), locale); return Collections.unmodifiableList(products); } /** - * Gets a products in the current language. + * Gets a product 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); + Product product = productRepository.findByI18nName(locale.getLanguage(), 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()); + return dtoFactory.createProductDTO(product, locale); } } diff --git a/src/main/java/de/mstock/monolith/web/CategoryDTO.java b/src/main/java/de/mstock/monolith/web/CategoryDTO.java index eccd3d1..4700eaf 100644 --- a/src/main/java/de/mstock/monolith/web/CategoryDTO.java +++ b/src/main/java/de/mstock/monolith/web/CategoryDTO.java @@ -2,20 +2,23 @@ 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; - } + 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/web/ProductController.java b/src/main/java/de/mstock/monolith/web/ProductController.java index 373f7f3..52f1ed3 100644 --- a/src/main/java/de/mstock/monolith/web/ProductController.java +++ b/src/main/java/de/mstock/monolith/web/ProductController.java @@ -2,14 +2,19 @@ package de.mstock.monolith.web; import java.util.Locale; +import javax.validation.Valid; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; 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.ReviewService; import de.mstock.monolith.service.ShopService; +import de.mstock.monolith.web.form.ReviewForm; @Controller public class ProductController { @@ -19,6 +24,9 @@ public class ProductController { @Autowired private ShopService shopService; + @Autowired + private ReviewService reviewService; + /** * Product page * @@ -28,7 +36,32 @@ public class ProductController { * @return The template's name. */ @RequestMapping(value = "/products/{prettyUrlFragment:[\\w-]+}", method = RequestMethod.GET) - public String homepage(@PathVariable String prettyUrlFragment, Model model, Locale locale) { + public String product(@PathVariable String prettyUrlFragment, Model model, Locale locale) { + model.addAttribute("categories", shopService.getCategories(locale)); + model.addAttribute("product", shopService.getProduct(locale, prettyUrlFragment)); + return TEMPLATE; + } + + /** + * Post a review + * + * @param reviewForm Form data + * @param bindingResult Form binding result after validation + * @param prettyUrlFragment Product context + * @param model Template model + * @param locale Language context + * @return The template's name. + */ + @RequestMapping(value = "/products/{prettyUrlFragment:[\\w-]+}", method = RequestMethod.POST) + public String post(@Valid ReviewForm reviewForm, BindingResult bindingResult, + @PathVariable String prettyUrlFragment, Model model, Locale locale) { + if (bindingResult.hasErrors()) { + model.addAttribute("success", false); + } else { + model.addAttribute("success", true); + model.addAttribute("reviewPost", + reviewService.saveReview(reviewForm, locale, prettyUrlFragment)); + } 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 index e4a7302..a5b76b6 100644 --- a/src/main/java/de/mstock/monolith/web/ProductDTO.java +++ b/src/main/java/de/mstock/monolith/web/ProductDTO.java @@ -1,57 +1,73 @@ package de.mstock.monolith.web; +import java.util.List; + 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; - } + private String itemNumber; + private ProductWeightUnit unit; + private String name; + private String prettyUrlFragment; + private String price; + private String description; + private List reviews; 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 String getName() { return name; } - public String getPrice() { - return price; - } - - public String getDescription() { - return description; + public void setName(String name) { + this.name = name; } public String getPrettyUrlFragment() { return prettyUrlFragment; } + public void setPrettyUrlFragment(String prettyUrlFragment) { + this.prettyUrlFragment = prettyUrlFragment; + } + + public String getPrice() { + return price; + } + + public void setPrice(String price) { + this.price = price; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getReviews() { + return reviews; + } + + public void setReviews(List reviews) { + this.reviews = reviews; + } + } diff --git a/src/main/java/de/mstock/monolith/web/ReviewDTO.java b/src/main/java/de/mstock/monolith/web/ReviewDTO.java new file mode 100644 index 0000000..4d9b3cc --- /dev/null +++ b/src/main/java/de/mstock/monolith/web/ReviewDTO.java @@ -0,0 +1,66 @@ +package de.mstock.monolith.web; + +import java.util.Locale; + +import org.apache.commons.lang3.StringUtils; + +public class ReviewDTO { + + private Locale locale; + private String firstName; + private String lastName; + private int ratingStars; + private String text; + + public String getLanguage() { + return locale.getLanguage(); + } + + public void setLanguage(String language) { + this.locale = new Locale(language); + } + + /** + * Presentation layer can use this method to access the full name of the reviewer. + * + * @return concatenated name + */ + public String getDisplayName() { + if (StringUtils.isNoneBlank(firstName, lastName)) { + return firstName + " " + lastName.charAt(0) + "."; + } + return StringUtils.defaultString(firstName); + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getRatingStars() { + return ratingStars; + } + + public void setRatingStars(int ratingStars) { + this.ratingStars = ratingStars; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/src/main/java/de/mstock/monolith/web/form/ReviewForm.java b/src/main/java/de/mstock/monolith/web/form/ReviewForm.java new file mode 100644 index 0000000..e84247a --- /dev/null +++ b/src/main/java/de/mstock/monolith/web/form/ReviewForm.java @@ -0,0 +1,53 @@ +package de.mstock.monolith.web.form; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +public class ReviewForm { + + @Size(max = 80) + private String firstName; + @Size(max = 80) + private String lastName; + @Min(1) + @Max(5) + @NotNull + private int ratingStars; + @Size(max = 1000) + private String text; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getRatingStars() { + return ratingStars; + } + + public void setRatingStars(int ratingStars) { + this.ratingStars = ratingStars; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + +} diff --git a/src/main/resources/db/migration/V3__foreign_key_i18n.sql b/src/main/resources/db/migration/V3__foreign_key_i18n.sql new file mode 100644 index 0000000..d99dcc3 --- /dev/null +++ b/src/main/resources/db/migration/V3__foreign_key_i18n.sql @@ -0,0 +1,9 @@ +alter table categories_i18n + add constraint categories_i18n_category_id_fkey foreign key (category_id) + references categories + on delete cascade; + +alter table products_i18n + add constraint products_i18n_product_id_fkey foreign key (product_id) + references products + on delete cascade; diff --git a/src/main/resources/db/migration/V4__create_reviews.sql b/src/main/resources/db/migration/V4__create_reviews.sql new file mode 100644 index 0000000..be2cf83 --- /dev/null +++ b/src/main/resources/db/migration/V4__create_reviews.sql @@ -0,0 +1,18 @@ +create table reviews ( + id serial primary key, + product_id integer, + locale_language varchar(2) not null, + first_name varchar(80), + last_name varchar(80), + rating_stars integer not null check (rating_stars between 1 and 5), + text varchar(1000), + foreign key (product_id, locale_language) references products_i18n(product_id, locale_language) +); + +insert into reviews (id, product_id, locale_language, first_name, last_name, rating_stars, text) values + (1, 1, 'en', 'John', 'Doe', 5, 'Absolutely perfect!'), + (2, 1, 'de', 'Max', 'Mustermann', 3, null), + (3, 1, 'de', 'Erika', 'Mustermann', 4, 'Ich liebe dieses Produkt! Leider ist ab und zu auch eine matschige Kiwi dabei.'), + (4, 4, 'de', 'Otto', 'Normalverbraucher', 4, 'Genau das was ich gesucht habe!'), + (5, 5, 'de', null, null, 1, 'Schmeckt nicht!'), + (6, 5, 'en', 'John', null, 2, 'What''s the country of origin of these tomatoes?'); diff --git a/src/main/resources/db/migration/V5__fix_sequences.sql b/src/main/resources/db/migration/V5__fix_sequences.sql new file mode 100644 index 0000000..f84c271 --- /dev/null +++ b/src/main/resources/db/migration/V5__fix_sequences.sql @@ -0,0 +1,3 @@ +alter sequence categories_id_seq restart with 100; +alter sequence products_id_seq restart with 100; +alter sequence reviews_id_seq restart with 100; diff --git a/src/main/resources/messages_de.properties b/src/main/resources/messages_de.properties index 1cbc288..6cb64d3 100644 --- a/src/main/resources/messages_de.properties +++ b/src/main/resources/messages_de.properties @@ -15,3 +15,6 @@ features.headline2 = Das ist gut. features.subheadline2 = Sieh' selbst. features.headline3 = Und zum Schluss, das hier. features.subheadline3 = Bingo. +reviews.title = Rezensionen +validation.review.success = Danke für deine Rezension! +validation.error = Es ist ein Fehler aufgetreten! diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties index c217fdd..b75cbb3 100644 --- a/src/main/resources/messages_en.properties +++ b/src/main/resources/messages_en.properties @@ -15,3 +15,6 @@ features.headline2 = Oh yeah, it's that good. features.subheadline2 = See for yourself. features.headline3 = And lastly, this one. features.subheadline3 = Checkmate. +reviews.title = Reviews +validation.review.success = Thanks for your review! +validation.error = An error occured! diff --git a/src/main/resources/templates/fragments/reviews.html b/src/main/resources/templates/fragments/reviews.html index 8d98f2e..b7c96c8 100644 --- a/src/main/resources/templates/fragments/reviews.html +++ b/src/main/resources/templates/fragments/reviews.html @@ -11,15 +11,66 @@
-

Reviews

-
+

Reviews

+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+   +   +   +   +   +
+
+
+ +
+ +
+
+ +
+
+
+

+ + + ★★ + ★★★ + ★★★★ + ★★★★★ + John D. +

+

Lorem ipsum dolor sit amet, + consetetur sadipscing elitr, sed diam nonumy eirmod tempor + invidunt ut labore et dolore magna aliquyam.

diff --git a/src/test/java/de/mstock/monolith/domain/DataTransferObjectFactoryTest.java b/src/test/java/de/mstock/monolith/domain/DataTransferObjectFactoryTest.java new file mode 100644 index 0000000..fce466f --- /dev/null +++ b/src/test/java/de/mstock/monolith/domain/DataTransferObjectFactoryTest.java @@ -0,0 +1,79 @@ +package de.mstock.monolith.domain; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.runners.MockitoJUnitRunner; + +import de.mstock.monolith.web.ProductDTO; + +@RunWith(MockitoJUnitRunner.class) +public class DataTransferObjectFactoryTest { + + @InjectMocks + private DataTransferObjectFactory dtoFactory; + + @Test + public void shouldCreateProductDTOWithoutReviews() { + Product product = mock(Product.class, RETURNS_DEEP_STUBS); + ProductI18n productI18n = mock(ProductI18n.class); + when(product.getI18n().get(any())).thenReturn(productI18n); + when(productI18n.getPrice()).thenReturn(BigDecimal.valueOf(1.23)); + verify(productI18n, never()).getReviews(); + List productDTOs = + dtoFactory.createProductWithoutReviewsDTOs(Arrays.asList(product), new Locale("de")); + assertThat("The review list is not present", productDTOs.get(0).getReviews(), is(nullValue())); + } + + @Test + public void shouldCreateProductDTOWithReviews() { + Product product = mock(Product.class, RETURNS_DEEP_STUBS); + Review review = mock(Review.class); + when(review.getLocaleLanguage()).thenReturn("de"); + when(product.getI18n().get(any()).getPrice()).thenReturn(BigDecimal.valueOf(1.23)); + when(product.getI18n().get(any()).getReviews()).thenReturn(Arrays.asList(review)); + List productDTOs = + dtoFactory.createProductDTOs(Arrays.asList(product), new Locale("de")); + assertThat("The review list is filled", productDTOs.get(0).getReviews(), is(not(empty()))); + } + + @Test + public void shouldCreateProductDTOWithEmptyReviews() { + Product product = mock(Product.class, RETURNS_DEEP_STUBS); + when(product.getI18n().get(any()).getPrice()).thenReturn(BigDecimal.valueOf(1.23)); + when(product.getI18n().get(any()).getReviews()).thenReturn(Collections.emptyList()); + List productDTOs = + dtoFactory.createProductDTOs(Arrays.asList(product), new Locale("de")); + assertThat("The review list is present, but empty", productDTOs.get(0).getReviews(), + is(empty())); + } + + @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)); + ProductDTO productDTO = dtoFactory.createProductDTO(product, locale); + assertThat("Product has a formatted price", productDTO.getPrice(), is(equalTo("$1.47"))); + } +} diff --git a/src/test/java/de/mstock/monolith/service/ShopServiceTest.java b/src/test/java/de/mstock/monolith/service/ShopServiceTest.java index 92cba32..f5f7025 100644 --- a/src/test/java/de/mstock/monolith/service/ShopServiceTest.java +++ b/src/test/java/de/mstock/monolith/service/ShopServiceTest.java @@ -3,13 +3,12 @@ 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.any; 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; @@ -22,10 +21,9 @@ 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.DataTransferObjectFactory; import de.mstock.monolith.domain.ProductRepository; import de.mstock.monolith.web.CategoryDTO; -import de.mstock.monolith.web.ProductDTO; @RunWith(MockitoJUnitRunner.class) public class ShopServiceTest { @@ -36,6 +34,9 @@ public class ShopServiceTest { @Mock private ProductRepository productRepository; + @Mock + private DataTransferObjectFactory dataTransferObjectFactory; + @InjectMocks private ShopService shopService; @@ -45,20 +46,11 @@ public class ShopServiceTest { Category category = mock(Category.class, RETURNS_DEEP_STUBS); List categoryEntities = Arrays.asList(category, category, category); when(categoryRepository.findAllOrdered(eq(locale.getLanguage()))).thenReturn(categoryEntities); + when(dataTransferObjectFactory.createCategoryDTO(any(), any())) + .thenReturn(mock(CategoryDTO.class)); 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"))); - } - }