genofire/hs_monolith
genofire
/
hs_monolith
Archived
1
0
Fork 0

Merge branch '2-add-reviews' into 'master'

Resolve "Add Reviews"

Closes #2 and #3

See merge request !4
This commit is contained in:
Matthias Stock 2017-03-29 22:08:41 +00:00
commit ee4696d737
22 changed files with 677 additions and 83 deletions

View File

@ -4,6 +4,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.JoinTable; import javax.persistence.JoinTable;
@ -17,6 +19,7 @@ import javax.persistence.Table;
public class Category { public class Category {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id; private int id;
private int ordinal; private int ordinal;
@OneToMany(mappedBy = "category") @OneToMany(mappedBy = "category")

View File

@ -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<ProductDTO> createProductDTOs(List<Product> products, Locale locale) {
List<ProductDTO> 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<ProductDTO> createProductWithoutReviewsDTOs(List<Product> products, Locale locale) {
List<ProductDTO> 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<ReviewDTO> createReviewDTOs(List<Review> reviews) {
List<ReviewDTO> ratingDTOs = new ArrayList<>(reviews.size());
for (Review review : reviews) {
ratingDTOs.add(createReviewDTO(review));
}
return Collections.unmodifiableList(ratingDTOs);
}
}

View File

@ -5,6 +5,8 @@ import java.util.Map;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.EnumType; import javax.persistence.EnumType;
import javax.persistence.Enumerated; import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.MapKey; import javax.persistence.MapKey;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
@ -15,6 +17,7 @@ import javax.persistence.Table;
public class Product { public class Product {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id; private int id;
private String itemNumber; private String itemNumber;
@Enumerated(EnumType.ORDINAL) @Enumerated(EnumType.ORDINAL)

View File

@ -1,11 +1,15 @@
package de.mstock.monolith.domain; package de.mstock.monolith.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List;
import javax.persistence.EmbeddedId; import javax.persistence.EmbeddedId;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.MapsId; import javax.persistence.MapsId;
import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
@Entity @Entity
@ -21,6 +25,9 @@ public class ProductI18n {
private String prettyUrlFragment; private String prettyUrlFragment;
private BigDecimal price; private BigDecimal price;
private String description; private String description;
@OneToMany
@JoinColumns({@JoinColumn(name = "localeLanguage"), @JoinColumn(name = "productId")})
private List<Review> reviews;
public ProductI18nPk getProductI18nPk() { public ProductI18nPk getProductI18nPk() {
return productI18nPk; return productI18nPk;
@ -61,4 +68,12 @@ public class ProductI18n {
public void setDescription(String description) { public void setDescription(String description) {
this.description = description; this.description = description;
} }
public List<Review> getReviews() {
return reviews;
}
public void setReviews(List<Review> reviews) {
this.reviews = reviews;
}
} }

View File

@ -5,7 +5,7 @@ import org.springframework.data.repository.Repository;
public interface ProductRepository extends Repository<Product, Integer> { public interface ProductRepository extends Repository<Product, Integer> {
@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") + "where key(i) = ?1 and lower(i.name) = ?2")
Product findByI18nName(String language, String name); Product findByI18nName(String language, String name);

View File

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

View File

@ -0,0 +1,7 @@
package de.mstock.monolith.domain;
import org.springframework.data.repository.CrudRepository;
public interface ReviewRepository extends CrudRepository<Review, Integer> {
}

View File

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

View File

@ -1,6 +1,5 @@
package de.mstock.monolith.service; package de.mstock.monolith.service;
import java.text.NumberFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -10,10 +9,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import de.mstock.monolith.domain.Category; import de.mstock.monolith.domain.Category;
import de.mstock.monolith.domain.CategoryI18n;
import de.mstock.monolith.domain.CategoryRepository; import de.mstock.monolith.domain.CategoryRepository;
import de.mstock.monolith.domain.DataTransferObjectFactory;
import de.mstock.monolith.domain.Product; import de.mstock.monolith.domain.Product;
import de.mstock.monolith.domain.ProductI18n;
import de.mstock.monolith.domain.ProductRepository; import de.mstock.monolith.domain.ProductRepository;
import de.mstock.monolith.web.CategoryDTO; import de.mstock.monolith.web.CategoryDTO;
import de.mstock.monolith.web.ProductDTO; import de.mstock.monolith.web.ProductDTO;
@ -27,6 +25,9 @@ public class ShopService {
@Autowired @Autowired
private ProductRepository productRepository; private ProductRepository productRepository;
@Autowired
private DataTransferObjectFactory dtoFactory;
/** /**
* Gets all categories of the current language. * Gets all categories of the current language.
* *
@ -36,8 +37,7 @@ public class ShopService {
String language = locale.getLanguage(); String language = locale.getLanguage();
List<CategoryDTO> categories = new ArrayList<>(); List<CategoryDTO> categories = new ArrayList<>();
for (Category category : categoryRepository.findAllOrdered(language)) { for (Category category : categoryRepository.findAllOrdered(language)) {
CategoryI18n i18n = category.getI18n().get(language); categories.add(dtoFactory.createCategoryDTO(category, locale));
categories.add(new CategoryDTO(i18n.getName(), i18n.getPrettyUrlFragment()));
} }
return Collections.unmodifiableList(categories); return Collections.unmodifiableList(categories);
} }
@ -53,33 +53,22 @@ public class ShopService {
if (category == null) { if (category == null) {
throw new NotFoundException(); throw new NotFoundException();
} }
List<ProductDTO> products = new ArrayList<>(); List<ProductDTO> products =
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(locale); dtoFactory.createProductWithoutReviewsDTOs(category.getProducts(), 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); return Collections.unmodifiableList(products);
} }
/** /**
* Gets a products in the current language. * Gets a product in the current language.
* *
* @return A simplified Data Transfer Object. * @return A simplified Data Transfer Object.
*/ */
public ProductDTO getProduct(Locale locale, String prettyUrlFragment) { public ProductDTO getProduct(Locale locale, String prettyUrlFragment) {
String language = locale.getLanguage(); Product product = productRepository.findByI18nName(locale.getLanguage(), prettyUrlFragment);
Product product = productRepository.findByI18nName(language, prettyUrlFragment);
if (product == null) { if (product == null) {
throw new NotFoundException(); throw new NotFoundException();
} }
ProductI18n i18n = product.getI18n().get(language); return dtoFactory.createProductDTO(product, locale);
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

@ -2,20 +2,23 @@ package de.mstock.monolith.web;
public class CategoryDTO { public class CategoryDTO {
private final String name; private String name;
private final String prettyUrlFragment; private String prettyUrlFragment;
public CategoryDTO(String name, String prettyUrlFragment) {
this.name = name;
this.prettyUrlFragment = prettyUrlFragment;
}
public String getName() { public String getName() {
return name; return name;
} }
public void setName(String name) {
this.name = name;
}
public String getPrettyUrlFragment() { public String getPrettyUrlFragment() {
return prettyUrlFragment; return prettyUrlFragment;
} }
public void setPrettyUrlFragment(String prettyUrlFragment) {
this.prettyUrlFragment = prettyUrlFragment;
}
} }

View File

@ -2,14 +2,19 @@ package de.mstock.monolith.web;
import java.util.Locale; import java.util.Locale;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; 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.validation.BindingResult;
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.ReviewService;
import de.mstock.monolith.service.ShopService; import de.mstock.monolith.service.ShopService;
import de.mstock.monolith.web.form.ReviewForm;
@Controller @Controller
public class ProductController { public class ProductController {
@ -19,6 +24,9 @@ public class ProductController {
@Autowired @Autowired
private ShopService shopService; private ShopService shopService;
@Autowired
private ReviewService reviewService;
/** /**
* Product page * Product page
* *
@ -28,7 +36,32 @@ public class ProductController {
* @return The template's name. * @return The template's name.
*/ */
@RequestMapping(value = "/products/{prettyUrlFragment:[\\w-]+}", method = RequestMethod.GET) @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("categories", shopService.getCategories(locale));
model.addAttribute("product", shopService.getProduct(locale, prettyUrlFragment)); model.addAttribute("product", shopService.getProduct(locale, prettyUrlFragment));
return TEMPLATE; return TEMPLATE;

View File

@ -1,57 +1,73 @@
package de.mstock.monolith.web; package de.mstock.monolith.web;
import java.util.List;
import de.mstock.monolith.domain.ProductWeightUnit; import de.mstock.monolith.domain.ProductWeightUnit;
public class ProductDTO { public class ProductDTO {
private final String itemNumber; private String itemNumber;
private final ProductWeightUnit unit; private ProductWeightUnit unit;
private final String name; private String name;
private final String prettyUrlFragment; private String prettyUrlFragment;
private final String price; private String price;
private final String description; private String description;
private List<ReviewDTO> reviews;
/**
* 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() { public String getItemNumber() {
return itemNumber; return itemNumber;
} }
public void setItemNumber(String itemNumber) {
this.itemNumber = itemNumber;
}
public ProductWeightUnit getUnit() { public ProductWeightUnit getUnit() {
return unit; return unit;
} }
public void setUnit(ProductWeightUnit unit) {
this.unit = unit;
}
public String getName() { public String getName() {
return name; return name;
} }
public String getPrice() { public void setName(String name) {
return price; this.name = name;
}
public String getDescription() {
return description;
} }
public String getPrettyUrlFragment() { public String getPrettyUrlFragment() {
return prettyUrlFragment; 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<ReviewDTO> getReviews() {
return reviews;
}
public void setReviews(List<ReviewDTO> reviews) {
this.reviews = reviews;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,3 +15,6 @@ features.headline2 = Das ist gut.
features.subheadline2 = Sieh' selbst. features.subheadline2 = Sieh' selbst.
features.headline3 = Und zum Schluss, das hier. features.headline3 = Und zum Schluss, das hier.
features.subheadline3 = Bingo. features.subheadline3 = Bingo.
reviews.title = Rezensionen
validation.review.success = Danke für deine Rezension!
validation.error = Es ist ein Fehler aufgetreten!

View File

@ -15,3 +15,6 @@ features.headline2 = Oh yeah, it's that good.
features.subheadline2 = See for yourself. features.subheadline2 = See for yourself.
features.headline3 = And lastly, this one. features.headline3 = And lastly, this one.
features.subheadline3 = Checkmate. features.subheadline3 = Checkmate.
reviews.title = Reviews
validation.review.success = Thanks for your review!
validation.error = An error occured!

View File

@ -11,15 +11,66 @@
<div class="container"> <div class="container">
<div class="row reviews" th:fragment="reviews"> <div class="row reviews" th:fragment="reviews">
<div class="col-md-12"> <div class="col-md-12">
<h2 th:inline="text">Reviews</h2> <h2 th:text="#{reviews.title}">Reviews</h2>
<div class="review"> <div th:if="${success != null and success}" class="alert alert-success" role="alert">
<p th:text="#{validation.review.success}">Thanks for your review!</p>
<p> <p>
<span class="rating"></span> Great product! <span class="rating" th:switch="${reviewPost.ratingStars}">
<th:block th:case="1"></th:block>
<th:block th:case="2">★★</th:block>
<th:block th:case="3">★★★</th:block>
<th:block th:case="4">★★★★</th:block>
<th:block th:case="5">★★★★★</th:block>
</span> <span th:text="${reviewPost.displayName}">John D.</span>
</p> </p>
<p>John D.</p> <p th:text="${reviewPost.text}">Lorem ipsum</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing </div>
elitr, sed diam nonumy eirmod tempor invidunt ut labore et <div th:if="${success != null and !success}" th:text="#{validation.error}" class="alert alert-danger" role="alert">An error occured!</div>
dolore magna aliquyam.</p> <form th:if="${success == null}" class="form-horizontal" action="#" th:action="@{/products/__${product.prettyUrlFragment}__.html}" th:object="${reviewPost}" method="post">
<div class="form-group">
<label for="firstName" class="col-sm-2 control-label">First Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="firstName" name="firstName" />
</div>
</div>
<div class="form-group">
<label for="lastName" class="col-sm-2 control-label">Last Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="lastName" name="lastName" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Rating</label>
<div class="col-sm-10">
<input type="radio" id="ratingStars1" name="ratingStars" value="1" checked="checked" />&nbsp;<label for="ratingStars1"></label>
<input type="radio" id="ratingStars2" name="ratingStars" value="2" />&nbsp;<label for="ratingStars2">★★</label>
<input type="radio" id="ratingStars3" name="ratingStars" value="3" />&nbsp;<label for="ratingStars3">★★★</label>
<input type="radio" id="ratingStars4" name="ratingStars" value="4" />&nbsp;<label for="ratingStars4">★★★★</label>
<input type="radio" id="ratingStars5" name="ratingStars" value="5" />&nbsp;<label for="ratingStars5">★★★★★</label>
</div>
</div>
<div class="form-group">
<label for="text" class="col-sm-2 control-label">Text</label>
<div class="col-sm-10">
<textarea class="form-control" id="text" name="text"></textarea>
</div>
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<hr />
<div class="review" th:each="review : ${product.reviews}">
<p>
<span class="rating" th:switch="${review.ratingStars}">
<th:block th:case="1"></th:block>
<th:block th:case="2">★★</th:block>
<th:block th:case="3">★★★</th:block>
<th:block th:case="4">★★★★</th:block>
<th:block th:case="5">★★★★★</th:block>
</span> <span th:text="${review.displayName}">John D.</span>
</p>
<p th:text="${review.text}">Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor
invidunt ut labore et dolore magna aliquyam.</p>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -3,13 +3,12 @@ package de.mstock.monolith.service;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat; 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.Matchers.eq;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -22,10 +21,9 @@ import org.mockito.runners.MockitoJUnitRunner;
import de.mstock.monolith.domain.Category; import de.mstock.monolith.domain.Category;
import de.mstock.monolith.domain.CategoryRepository; 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.domain.ProductRepository;
import de.mstock.monolith.web.CategoryDTO; import de.mstock.monolith.web.CategoryDTO;
import de.mstock.monolith.web.ProductDTO;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class ShopServiceTest { public class ShopServiceTest {
@ -36,6 +34,9 @@ public class ShopServiceTest {
@Mock @Mock
private ProductRepository productRepository; private ProductRepository productRepository;
@Mock
private DataTransferObjectFactory dataTransferObjectFactory;
@InjectMocks @InjectMocks
private ShopService shopService; private ShopService shopService;
@ -45,20 +46,11 @@ public class ShopServiceTest {
Category category = mock(Category.class, RETURNS_DEEP_STUBS); Category category = mock(Category.class, RETURNS_DEEP_STUBS);
List<Category> categoryEntities = Arrays.asList(category, category, category); List<Category> categoryEntities = Arrays.asList(category, category, category);
when(categoryRepository.findAllOrdered(eq(locale.getLanguage()))).thenReturn(categoryEntities); when(categoryRepository.findAllOrdered(eq(locale.getLanguage()))).thenReturn(categoryEntities);
when(dataTransferObjectFactory.createCategoryDTO(any(), any()))
.thenReturn(mock(CategoryDTO.class));
List<CategoryDTO> categories = shopService.getCategories(locale); List<CategoryDTO> categories = shopService.getCategories(locale);
assertThat("Same amount of categories", categories.size(), assertThat("Same amount of categories", categories.size(),
is(equalTo(categoryEntities.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")));
}
} }