T checkNotFound(T object, String msg) {
+ checkNotFound(object != null, msg);
+ return object;
+ }
+
+ public static void checkNotFound(boolean found, String msg) {
+ if (!found) {
+ throw new NotFoundException("Not found entity with " + msg);
+ }
+ }
+
+ public static void checkNew(BaseEntity entity) {
+ if (!entity.isNew()) {
+ throw new IllegalArgumentException(entity + " must be new (id=null)");
+ }
+ }
+
+ public static void checkIdConsistent(BaseEntity entity, int id) {
+// http://stackoverflow.com/a/32728226/548473
+ if (entity.isNew()) {
+ entity.setId(id);
+ } else if (entity.getId() != id) {
+ throw new IllegalArgumentException(entity + " must be with id=" + id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
new file mode 100644
index 000000000000..f1e9b0e46376
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
@@ -0,0 +1,7 @@
+package ru.javawebinar.topjava.util.exception;
+
+public class NotFoundException extends RuntimeException {
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java
new file mode 100644
index 000000000000..6766757755f9
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java
@@ -0,0 +1,47 @@
+package ru.javawebinar.topjava.web;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.service.MealService;
+import ru.javawebinar.topjava.service.UserService;
+import ru.javawebinar.topjava.util.MealsUtil;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Controller
+public class RootController {
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private MealService mealService;
+
+ @GetMapping("/")
+ public String root() {
+ return "index";
+ }
+
+ @GetMapping("/users")
+ public String users(Model model) {
+ model.addAttribute("users", userService.getAll());
+ return "users";
+ }
+
+ @PostMapping("/users")
+ public String setUser(HttpServletRequest request) {
+ int userId = Integer.valueOf(request.getParameter("userId"));
+ AuthorizedUser.setId(userId);
+ return "redirect:meals";
+ }
+
+ @GetMapping("/meals")
+ public String meals(Model model) {
+ model.addAttribute("meals",
+ MealsUtil.getWithExceeded(mealService.getAll(AuthorizedUser.id()), AuthorizedUser.getCaloriesPerDay()));
+ return "meals";
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java
new file mode 100644
index 000000000000..742794ba8479
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava.web.json;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+/**
+ *
+ * Handling Hibernate lazy-loading
+ *
+ * @link https://github.com/FasterXML/jackson
+ * @link https://github.com/FasterXML/jackson-datatype-hibernate
+ * @link http://wiki.fasterxml.com/JacksonHowToCustomSerializers
+ */
+public class JacksonObjectMapper extends ObjectMapper {
+
+ private static final ObjectMapper MAPPER = new JacksonObjectMapper();
+
+ public static ObjectMapper getMapper() {
+ return MAPPER;
+ }
+
+ private JacksonObjectMapper() {
+ registerModule(new Hibernate5Module());
+
+ registerModule(new JavaTimeModule());
+ configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+
+ setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
+ setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
new file mode 100644
index 000000000000..f230bf1d9a87
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava.web.json;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+import java.io.IOException;
+import java.util.List;
+
+import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper;
+
+public class JsonUtil {
+
+ public static List readValues(String json, Class clazz) {
+ ObjectReader reader = getMapper().readerFor(clazz);
+ try {
+ return reader.readValues(json).readAll();
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Invalid read array from JSON:\n'" + json + "'", e);
+ }
+ }
+
+ public static T readValue(String json, Class clazz) {
+ try {
+ return getMapper().readValue(json, clazz);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Invalid read from JSON:\n'" + json + "'", e);
+ }
+ }
+
+ public static String writeValue(T obj) {
+ try {
+ return getMapper().writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e);
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
new file mode 100644
index 000000000000..ce8649307666
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
@@ -0,0 +1,71 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.service.MealService;
+import ru.javawebinar.topjava.to.MealWithExceed;
+import ru.javawebinar.topjava.util.DateTimeUtil;
+import ru.javawebinar.topjava.util.MealsUtil;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.checkIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNew;
+
+public abstract class AbstractMealController {
+ private final Logger LOG = LoggerFactory.getLogger(getClass());
+
+ @Autowired
+ private MealService service;
+
+ public Meal get(int id) {
+ int userId = AuthorizedUser.id();
+ LOG.info("get meal {} for User {}", id, userId);
+ return service.get(id, userId);
+ }
+
+ public void delete(int id) {
+ int userId = AuthorizedUser.id();
+ LOG.info("delete meal {} for User {}", id, userId);
+ service.delete(id, userId);
+ }
+
+ public List getAll() {
+ int userId = AuthorizedUser.id();
+ LOG.info("getAll for User {}", userId);
+ return MealsUtil.getWithExceeded(service.getAll(userId), AuthorizedUser.getCaloriesPerDay());
+ }
+
+ public Meal create(Meal meal) {
+ int userId = AuthorizedUser.id();
+ checkNew(meal);
+ LOG.info("create {} for User {}", meal, userId);
+ return service.save(meal, userId);
+ }
+
+ public void update(Meal meal, int id) {
+ int userId = AuthorizedUser.id();
+ checkIdConsistent(meal, id);
+ LOG.info("update {} for User {}", meal, userId);
+ service.update(meal, userId);
+ }
+
+ public List getBetween(LocalDate startDate, LocalTime startTime, LocalDate endDate, LocalTime endTime) {
+ int userId = AuthorizedUser.id();
+ LOG.info("getBetween dates({} - {}) time({} - {}) for User {}", startDate, endDate, startTime, endTime, userId);
+
+ return MealsUtil.getFilteredWithExceeded(
+ service.getBetweenDates(
+ startDate != null ? startDate : DateTimeUtil.MIN_DATE,
+ endDate != null ? endDate : DateTimeUtil.MAX_DATE, userId),
+ startTime != null ? startTime : LocalTime.MIN,
+ endTime != null ? endTime : LocalTime.MAX,
+ AuthorizedUser.getCaloriesPerDay()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java
new file mode 100644
index 000000000000..24debce3b0b7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java
@@ -0,0 +1,70 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import ru.javawebinar.topjava.model.Meal;
+
+import javax.servlet.http.HttpServletRequest;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Objects;
+
+import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate;
+import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime;
+
+@Controller
+@RequestMapping(value = "/meals")
+public class JspMealController extends AbstractMealController {
+
+ @GetMapping("/delete")
+ public String delete(HttpServletRequest request) {
+ super.delete(getId(request));
+ return "redirect:/meals";
+ }
+
+ @GetMapping("/update")
+ public String update(HttpServletRequest request, Model model) {
+ model.addAttribute("meal", super.get(getId(request)));
+ return "meal";
+ }
+
+ @GetMapping("/create")
+ public String create(Model model) {
+ model.addAttribute("meal", new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS), "", 1000));
+ return "meal";
+ }
+
+ @PostMapping
+ public String updateOrCreate(HttpServletRequest request) {
+ Meal meal = new Meal(LocalDateTime.parse(request.getParameter("dateTime")),
+ request.getParameter("description"),
+ Integer.valueOf(request.getParameter("calories")));
+
+ if (request.getParameter("id").isEmpty()) {
+ super.create(meal);
+ } else {
+ super.update(meal, getId(request));
+ }
+ return "redirect:/meals";
+ }
+
+ @PostMapping("/filter")
+ public String getBetween(HttpServletRequest request, Model model) {
+ LocalDate startDate = parseLocalDate(request.getParameter("startDate"));
+ LocalDate endDate = parseLocalDate(request.getParameter("endDate"));
+ LocalTime startTime = parseLocalTime(request.getParameter("startTime"));
+ LocalTime endTime = parseLocalTime(request.getParameter("endTime"));
+ model.addAttribute("meals", super.getBetween(startDate, startTime, endDate, endTime));
+ return "meals";
+ }
+
+ private int getId(HttpServletRequest request) {
+ String paramId = Objects.requireNonNull(request.getParameter("id"));
+ return Integer.valueOf(paramId);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
new file mode 100644
index 000000000000..c3daf68535d7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
@@ -0,0 +1,7 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.springframework.stereotype.Controller;
+
+@Controller
+public class MealRestController extends AbstractMealController {
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
new file mode 100644
index 000000000000..39b8d4fdfa34
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -0,0 +1,53 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.checkIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNew;
+
+public abstract class AbstractUserController {
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ private final UserService service;
+
+ public AbstractUserController(UserService service) {
+ this.service = service;
+ }
+
+ public List getAll() {
+ log.info("getAll");
+ return service.getAll();
+ }
+
+ public User get(int id) {
+ log.info("get {}", id);
+ return service.get(id);
+ }
+
+ public User create(User user) {
+ log.info("create {}", user);
+ checkNew(user);
+ return service.save(user);
+ }
+
+ public void delete(int id) {
+ log.info("delete {}", id);
+ service.delete(id);
+ }
+
+ public void update(User user, int id) {
+ log.info("update {}", user);
+ checkIdConsistent(user, id);
+ service.update(user);
+ }
+
+ public User getByMail(String email) {
+ log.info("getByEmail {}", email);
+ return service.getByEmail(email);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
new file mode 100644
index 000000000000..a1f4be3a93ea
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
@@ -0,0 +1,67 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+
+import java.net.URI;
+import java.util.List;
+
+@RestController
+@RequestMapping(AdminRestController.REST_URL)
+public class AdminRestController extends AbstractUserController {
+ static final String REST_URL = "/rest/admin/users";
+
+ @Autowired
+ public AdminRestController(UserService service) {
+ super(service);
+ }
+
+ @Override
+ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+ public User get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@RequestBody User user) {
+ User created = super.create(user);
+
+// HttpHeaders httpHeaders = new HttpHeaders();
+// httpHeaders.setLocation(uriOfNewResource);
+
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL + "/{id}")
+ .buildAndExpand(created.getId()).toUri();
+
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @Override
+ @DeleteMapping(value = "/{id}")
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @Override
+ @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
+ public void update(@RequestBody User user, @PathVariable("id") int id) {
+ super.update(user, id);
+ }
+
+ @Override
+ @GetMapping(value = "/by", produces = MediaType.APPLICATION_JSON_VALUE)
+ public User getByMail(@RequestParam("email") String email) {
+ return super.getByMail(email);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
new file mode 100644
index 000000000000..8da19b413b6e
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,39 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+
+@RestController
+@RequestMapping(ProfileRestController.REST_URL)
+public class ProfileRestController extends AbstractUserController {
+ static final String REST_URL = "/rest/profile";
+
+ @Autowired
+ public ProfileRestController(UserService service) {
+ super(service);
+ }
+
+ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ public User get() {
+ return super.get(AuthorizedUser.id());
+ }
+
+ @DeleteMapping
+ public void delete() {
+ super.delete(AuthorizedUser.id());
+ }
+
+ @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ public void update(@RequestBody User user) {
+ super.update(user, AuthorizedUser.id());
+ }
+
+ @GetMapping(value = "/text")
+ public String testUTF() {
+ return "Русский текст";
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml
new file mode 100644
index 000000000000..b0275e589a4a
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/cache/ehcache.xsd b/src/main/resources/cache/ehcache.xsd
new file mode 100644
index 000000000000..bfc19ddb1e7d
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xsd
@@ -0,0 +1,419 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties
new file mode 100644
index 000000000000..aeeb57462caf
--- /dev/null
+++ b/src/main/resources/db/hsqldb.properties
@@ -0,0 +1,12 @@
+#database.url=jdbc:hsqldb:file:D:/temp/topjava
+database.url=jdbc:hsqldb:mem:topjava
+
+database.username=sa
+database.password=
+
+database.init=true
+
+jdbc.initLocation=initDB_hsql.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql
new file mode 100644
index 000000000000..aede3736c9d0
--- /dev/null
+++ b/src/main/resources/db/initDB.sql
@@ -0,0 +1,36 @@
+DROP TABLE IF EXISTS user_roles;
+DROP TABLE IF EXISTS meals;
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS global_seq;
+
+CREATE SEQUENCE global_seq START 100000;
+
+CREATE TABLE users
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ name VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ password VARCHAR NOT NULL,
+ registered TIMESTAMP DEFAULT now(),
+ enabled BOOL DEFAULT TRUE,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
+
+CREATE TABLE user_roles
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meals (
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ user_id INTEGER NOT NULL,
+ date_time TIMESTAMP NOT NULL,
+ description TEXT NOT NULL,
+ calories INT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals(user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql
new file mode 100644
index 000000000000..3ac041a8d1e8
--- /dev/null
+++ b/src/main/resources/db/initDB_hsql.sql
@@ -0,0 +1,37 @@
+DROP TABLE user_roles IF EXISTS;
+DROP TABLE meals IF EXISTS;
+DROP TABLE users IF EXISTS;
+DROP SEQUENCE global_seq IF EXISTS;
+
+CREATE SEQUENCE GLOBAL_SEQ AS INTEGER START WITH 100000;
+
+CREATE TABLE users
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ name VARCHAR(255),
+ email VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ registered TIMESTAMP DEFAULT now(),
+ enabled BOOLEAN DEFAULT TRUE,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON USERS (email);
+
+CREATE TABLE user_roles
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR(255),
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY ( user_id ) REFERENCES USERS (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meals
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ date_time TIMESTAMP NOT NULL,
+ description VARCHAR(255) NOT NULL,
+ calories INT NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY ( user_id ) REFERENCES USERS (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals (user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql
new file mode 100644
index 000000000000..cc816500ce36
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,25 @@
+DELETE FROM user_roles;
+DELETE FROM meals;
+DELETE FROM users;
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+INSERT INTO users (name, email, password)
+VALUES ('User', 'user@yandex.ru', 'password');
+
+INSERT INTO users (name, email, password)
+VALUES ('Admin', 'admin@gmail.com', 'admin');
+
+INSERT INTO user_roles (role, user_id) VALUES
+ ('ROLE_USER', 100000),
+ ('ROLE_ADMIN', 100001),
+ ('ROLE_USER', 100001);
+
+INSERT INTO meals (date_time, description, calories, user_id) VALUES
+ ('2015-05-30 10:00:00', 'Завтрак', 500, 100000),
+ ('2015-05-30 13:00:00', 'Обед', 1000, 100000),
+ ('2015-05-30 20:00:00', 'Ужин', 500, 100000),
+ ('2015-05-31 10:00:00', 'Завтрак', 500, 100000),
+ ('2015-05-31 13:00:00', 'Обед', 1000, 100000),
+ ('2015-05-31 20:00:00', 'Ужин', 510, 100000),
+ ('2015-06-01 14:00:00', 'Админ ланч', 510, 100001),
+ ('2015-06-01 21:00:00', 'Админ ужин', 1500, 100001);
diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties
new file mode 100644
index 000000000000..8d4b680629ff
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,14 @@
+#database.url=jdbc:postgresql://ec2-54-217-202-110.eu-west-1.compute.amazonaws.com:5432/dehm6lvm8bink0?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory
+#database.username=wegxlfzjjgxaxy
+#database.password=SSQyKKE_e93kiUCR-ehzMcKCxZ
+
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=user
+database.password=password
+
+database.init=true
+
+jdbc.initLocation=initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties
new file mode 100644
index 000000000000..09ed9c2548ee
--- /dev/null
+++ b/src/main/resources/db/tomcat.properties
@@ -0,0 +1,7 @@
+database.init=false
+
+jdbc.initLocation=initDB.sql
+
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 000000000000..8616ff8e73fc
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ ${TOPJAVA_ROOT}/log/topjava.log
+
+
+ UTF-8
+ %date %-5level %logger{0} [%file:%line] %msg%n
+
+
+
+
+
+ UTF-8
+ %-5level %logger{0} [%file:%line] %msg%n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml
new file mode 100644
index 000000000000..c3e7587cfdea
--- /dev/null
+++ b/src/main/resources/spring/spring-app.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml
new file mode 100644
index 000000000000..8cd84b0a37fd
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml
new file mode 100644
index 000000000000..79f7d6e3fff4
--- /dev/null
+++ b/src/main/resources/spring/spring-mvc.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ text/plain;charset=UTF-8
+ text/html;charset=UTF-8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-tools.xml b/src/main/resources/spring/spring-tools.xml
new file mode 100644
index 000000000000..c97ed199a023
--- /dev/null
+++ b/src/main/resources/spring/spring-tools.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/tomcat/context.xml b/src/main/resources/tomcat/context.xml
new file mode 100644
index 000000000000..9311d5904aea
--- /dev/null
+++ b/src/main/resources/tomcat/context.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+ WEB-INF/web.xml
+ ${catalina.base}/conf/web.xml
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp
new file mode 100644
index 000000000000..fcafd0e813f2
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp
@@ -0,0 +1,4 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp
new file mode 100644
index 000000000000..0935c441a36b
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp
@@ -0,0 +1,4 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp
new file mode 100644
index 000000000000..0c77f10859f8
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp
@@ -0,0 +1,10 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp
new file mode 100644
index 000000000000..4a1d77633c16
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/index.jsp
@@ -0,0 +1,24 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/meal.jsp b/src/main/webapp/WEB-INF/jsp/meal.jsp
new file mode 100644
index 000000000000..73228e80925e
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/meal.jsp
@@ -0,0 +1,34 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp
new file mode 100644
index 000000000000..1847501ef1bb
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/meals.jsp
@@ -0,0 +1,63 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%>
+ <%--<%=TimeUtil.toString(meal.getDateTime())%>--%>
+ ${fn:formatDateTime(meal.dateTime)}
+
+ ${meal.description}
+ ${meal.calories}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp
new file mode 100644
index 000000000000..8623615f1c05
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/users.jsp
@@ -0,0 +1,39 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${user.email}
+ ${user.roles}
+ <%=user.isEnabled()%>
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld
new file mode 100644
index 000000000000..d138fecdbfb5
--- /dev/null
+++ b/src/main/webapp/WEB-INF/tld/functions.tld
@@ -0,0 +1,16 @@
+
+
+
+ 1.0
+ functions
+ http://topjava.javawebinar.ru/functions
+
+
+ formatDateTime
+ ru.javawebinar.topjava.util.DateTimeUtil
+ java.lang.String toString(java.time.LocalDateTime)
+
+
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000000..6e1280c56d07
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,56 @@
+
+ Topjava
+
+
+ spring.profiles.default
+ postgres,datajpa
+
+
+
+ contextConfigLocation
+
+ classpath:spring/spring-app.xml
+ classpath:spring/spring-db.xml
+
+
+
+
+
+ org.springframework.web.context.ContextLoaderListener
+
+
+ mvc-dispatcher
+ org.springframework.web.servlet.DispatcherServlet
+
+ contextConfigLocation
+ classpath:spring/spring-mvc.xml
+
+ 1
+
+
+ mvc-dispatcher
+ /
+
+
+
+ encodingFilter
+ org.springframework.web.filter.CharacterEncodingFilter
+
+ encoding
+ UTF-8
+
+
+ forceEncoding
+ true
+
+
+
+ encodingFilter
+ /*
+
+
+
diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css
new file mode 100644
index 000000000000..36305de387ee
--- /dev/null
+++ b/src/main/webapp/resources/css/style.css
@@ -0,0 +1,32 @@
+dl {
+ background: none repeat scroll 0 0 #FAFAFA;
+ margin: 8px 0;
+ padding: 0;
+}
+
+dt {
+ display: inline-block;
+ width: 170px;
+}
+
+dd {
+ display: inline-block;
+ margin-left: 8px;
+ vertical-align: top;
+}
+
+.normal {
+ color: green;
+}
+
+.exceeded {
+ color: red;
+}
+
+header, footer {
+ background: none repeat scroll 0 0 #A6C9E2;
+ color: #2E6E9E;
+ font-size: 20px;
+ padding: 5px 20px;
+ margin: 6px 0;
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java
new file mode 100644
index 000000000000..05d47b19f214
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java
@@ -0,0 +1,12 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.test.context.ActiveProfilesResolver;
+
+//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver
+public class ActiveDbProfileResolver implements ActiveProfilesResolver {
+
+ @Override
+ public String[] resolve(Class> aClass) {
+ return new String[]{Profiles.getActiveDbProfile()};
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java b/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java
new file mode 100644
index 000000000000..4d1d1e9e3789
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java
@@ -0,0 +1,12 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.test.context.ActiveProfilesResolver;
+
+//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver
+public class AllActiveProfileResolver implements ActiveProfilesResolver {
+
+ @Override
+ public String[] resolve(Class> aClass) {
+ return new String[]{Profiles.REPOSITORY_IMPLEMENTATION, Profiles.getActiveDbProfile()};
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java
new file mode 100644
index 000000000000..d845f2772163
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java
@@ -0,0 +1,38 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.matcher.ModelMatcher;
+import ru.javawebinar.topjava.model.Meal;
+
+import java.time.Month;
+import java.util.Arrays;
+import java.util.List;
+
+import static java.time.LocalDateTime.of;
+import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ;
+
+public class MealTestData {
+
+ public static final ModelMatcher MATCHER = ModelMatcher.of(Meal.class);
+
+ public static final int MEAL1_ID = START_SEQ + 2;
+ public static final int ADMIN_MEAL_ID = START_SEQ + 8;
+
+ public static final Meal MEAL1 = new Meal(MEAL1_ID, of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500);
+ public static final Meal MEAL2 = new Meal(MEAL1_ID + 1, of(2015, Month.MAY, 30, 13, 0), "Обед", 1000);
+ public static final Meal MEAL3 = new Meal(MEAL1_ID + 2, of(2015, Month.MAY, 30, 20, 0), "Ужин", 500);
+ public static final Meal MEAL4 = new Meal(MEAL1_ID + 3, of(2015, Month.MAY, 31, 10, 0), "Завтрак", 500);
+ public static final Meal MEAL5 = new Meal(MEAL1_ID + 4, of(2015, Month.MAY, 31, 13, 0), "Обед", 1000);
+ public static final Meal MEAL6 = new Meal(MEAL1_ID + 5, of(2015, Month.MAY, 31, 20, 0), "Ужин", 510);
+ public static final Meal ADMIN_MEAL1 = new Meal(ADMIN_MEAL_ID, of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510);
+ public static final Meal ADMIN_MEAL2 = new Meal(ADMIN_MEAL_ID + 1, of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500);
+
+ public static final List MEALS = Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1);
+
+ public static Meal getCreated() {
+ return new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Созданный ужин", 300);
+ }
+
+ public static Meal getUpdated() {
+ return new Meal(MEAL1_ID, MEAL1.getDateTime(), "Обновленный завтрак", 200);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java
new file mode 100644
index 000000000000..070680d64fce
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.context.support.GenericXmlApplicationContext;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.MealWithExceed;
+import ru.javawebinar.topjava.web.meal.MealRestController;
+import ru.javawebinar.topjava.web.user.AdminRestController;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.Month;
+import java.util.Arrays;
+import java.util.List;
+
+public class SpringMain {
+ public static void main(String[] args) {
+ // java 7 Automatic resource management
+ try (GenericXmlApplicationContext appCtx = new GenericXmlApplicationContext()) {
+ appCtx.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION);
+ appCtx.load("spring/spring-app.xml", "spring/mock.xml");
+ appCtx.refresh();
+
+ System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames()));
+ AdminRestController adminUserController = appCtx.getBean(AdminRestController.class);
+ adminUserController.create(new User(null, "userName", "email", "password", Role.ROLE_ADMIN));
+ System.out.println();
+
+ MealRestController mealController = appCtx.getBean(MealRestController.class);
+ List filteredMealsWithExceeded =
+ mealController.getBetween(
+ LocalDate.of(2015, Month.MAY, 30), LocalTime.of(7, 0),
+ LocalDate.of(2015, Month.MAY, 31), LocalTime.of(11, 0));
+ filteredMealsWithExceeded.forEach(System.out::println);
+ }
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/TestUtil.java b/src/test/java/ru/javawebinar/topjava/TestUtil.java
new file mode 100644
index 000000000000..8132aaf0d4a5
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java
@@ -0,0 +1,17 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.test.web.servlet.ResultActions;
+
+import java.io.UnsupportedEncodingException;
+
+public class TestUtil {
+
+ public static ResultActions print(ResultActions action) throws UnsupportedEncodingException {
+ System.out.println(getContent(action));
+ return action;
+ }
+
+ public static String getContent(ResultActions action) throws UnsupportedEncodingException {
+ return action.andReturn().getResponse().getContentAsString();
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java
new file mode 100644
index 000000000000..e84882da888e
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java
@@ -0,0 +1,29 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.matcher.ModelMatcher;
+import ru.javawebinar.topjava.model.User;
+
+import java.util.Objects;
+
+import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ;
+
+public class UserTestData {
+ public static final int USER_ID = START_SEQ;
+ public static final int ADMIN_ID = START_SEQ + 1;
+
+ public static final User USER = new User(USER_ID, "User", "user@yandex.ru", "password", Role.ROLE_USER);
+ public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ROLE_ADMIN, Role.ROLE_USER);
+
+ public static final ModelMatcher MATCHER = ModelMatcher.of(User.class,
+ (expected, actual) -> expected == actual ||
+ (Objects.equals(expected.getPassword(), actual.getPassword())
+ && Objects.equals(expected.getId(), actual.getId())
+ && Objects.equals(expected.getName(), actual.getName())
+ && Objects.equals(expected.getEmail(), actual.getEmail())
+ && Objects.equals(expected.getCaloriesPerDay(), actual.getCaloriesPerDay())
+ && Objects.equals(expected.isEnabled(), actual.isEnabled())
+ && Objects.equals(expected.getRoles(), actual.getRoles())
+ )
+ );
+}
diff --git a/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java b/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java
new file mode 100644
index 000000000000..10fa74a6fed7
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java
@@ -0,0 +1,130 @@
+package ru.javawebinar.topjava.matcher;
+
+import org.junit.Assert;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+import ru.javawebinar.topjava.TestUtil;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+
+/**
+ * This test Matcher assert equality of beans and collections
+ *
+ * It wraps every entity by Wrapper before apply to Assert.assertEquals
+ * and every element of collection before call Collection equals
+ * in order to compare them by custom Equality.
+ *
+ * Default equality is String.valueOf(entity)
+ *
+ * Assert json response body with expected result, converting it via {@link JsonUtil}
+ * into object(collection) and wrap results by Wrapper.
+ *
+ * @param : Entity
+ */
+public class ModelMatcher {
+
+ private Equality equality;
+ private Class entityClass;
+
+ public interface Equality {
+ boolean areEqual(T expected, T actual);
+ }
+
+ private ModelMatcher(Class entityClass, Equality equality) {
+ this.entityClass = entityClass;
+ this.equality = equality;
+ }
+
+ public static ModelMatcher of(Class entityClass) {
+ return of(entityClass, (T expected, T actual) -> expected == actual || String.valueOf(expected).equals(String.valueOf(actual)));
+ }
+
+ public static ModelMatcher of(Class entityClass, Equality equality) {
+ return new ModelMatcher<>(entityClass, equality);
+ }
+
+ private class Wrapper {
+ private T entity;
+
+ private Wrapper(T entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Wrapper that = (Wrapper) o;
+ return entity != null ? equality.areEqual(entity, that.entity) : that.entity == null;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(entity);
+ }
+ }
+
+ private T fromJsonValue(String json) {
+ return JsonUtil.readValue(json, entityClass);
+ }
+
+ private Collection fromJsonValues(String json) {
+ return JsonUtil.readValues(json, entityClass);
+ }
+
+ public T fromJsonAction(ResultActions action) throws UnsupportedEncodingException {
+ return fromJsonValue(TestUtil.getContent(action));
+ }
+
+ public void assertEquals(T expected, T actual) {
+ Assert.assertEquals(wrap(expected), wrap(actual));
+ }
+
+ public void assertCollectionEquals(Collection expected, Collection actual) {
+ Assert.assertEquals(wrap(expected), wrap(actual));
+ }
+
+ private Wrapper wrap(T entity) {
+ return new Wrapper(entity);
+ }
+
+ private List wrap(Collection collection) {
+ return collection.stream().map(this::wrap).collect(Collectors.toList());
+ }
+
+ public ResultMatcher contentMatcher(T expect) {
+ return content().string(
+ new TestMatcher(expect) {
+ @Override
+ protected boolean compare(T expected, String body) {
+ Wrapper expectedForCompare = wrap(expected);
+ Wrapper actualForCompare = wrap(fromJsonValue(body));
+ return expectedForCompare.equals(actualForCompare);
+ }
+ });
+ }
+
+ @SafeVarargs
+ public final ResultMatcher contentListMatcher(T... expected) {
+ return contentListMatcher(Arrays.asList(expected));
+ }
+
+ public final ResultMatcher contentListMatcher(List expected) {
+ return content().string(
+ new TestMatcher>(expected) {
+ @Override
+ protected boolean compare(List expected, String actual) {
+ List expectedList = wrap(expected);
+ List actualList = wrap(fromJsonValues(actual));
+ return expectedList.equals(actualList);
+ }
+ });
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java b/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java
new file mode 100644
index 000000000000..45ac0f7d8a06
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java
@@ -0,0 +1,25 @@
+package ru.javawebinar.topjava.matcher;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+abstract public class TestMatcher extends BaseMatcher {
+ protected T expected;
+
+ public TestMatcher(T expected) {
+ this.expected = expected;
+ }
+
+ @Override
+ public boolean matches(Object actual) {
+ return compare(expected, (String) actual);
+ }
+
+ protected abstract boolean compare(T expected, String actual);
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(JsonUtil.writeValue(expected));
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryMealRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryMealRepositoryImpl.java
new file mode 100644
index 000000000000..388e8162bbda
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryMealRepositoryImpl.java
@@ -0,0 +1,81 @@
+package ru.javawebinar.topjava.repository.mock;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+import ru.javawebinar.topjava.util.DateTimeUtil;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Repository
+public class InMemoryMealRepositoryImpl implements MealRepository {
+ private static final Logger LOG = LoggerFactory.getLogger(InMemoryUserRepositoryImpl.class);
+
+ // Map userId -> (mealId-> meal)
+ private Map> repository = new ConcurrentHashMap<>();
+ private AtomicInteger counter = new AtomicInteger(0);
+
+ @Override
+ public Meal save(Meal meal, int userId) {
+ if (meal.isNew()) {
+ meal.setId(counter.incrementAndGet());
+ } else if (get(meal.getId(), userId) == null) {
+ return null;
+ }
+ Map meals = repository.computeIfAbsent(userId, ConcurrentHashMap::new);
+ meals.put(meal.getId(), meal);
+ return meal;
+ }
+
+ @PostConstruct
+ public void postConstruct() {
+ LOG.info("+++ PostConstruct");
+ }
+
+ @PreDestroy
+ public void preDestroy() {
+ LOG.info("+++ PreDestroy");
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ Map meals = repository.get(userId);
+ return meals != null && meals.remove(id) != null;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ Map meals = repository.get(userId);
+ return meals == null ? null : meals.get(id);
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return getAllAsStream(userId).collect(Collectors.toList());
+ }
+
+ @Override
+ public List getBetween(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ return getAllAsStream(userId)
+ .filter(um -> DateTimeUtil.isBetween(um.getDateTime(), startDateTime, endDateTime))
+ .collect(Collectors.toList());
+ }
+
+ private Stream getAllAsStream(int userId) {
+ Map meals = repository.get(userId);
+ return meals == null ?
+ Stream.empty() : meals.values().stream().sorted(Comparator.comparing(Meal::getDateTime).reversed());
+ }
+}
+
diff --git a/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java
new file mode 100644
index 000000000000..a8464fd23657
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java
@@ -0,0 +1,53 @@
+package ru.javawebinar.topjava.repository.mock;
+
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+@Repository
+public class InMemoryUserRepositoryImpl implements UserRepository {
+
+ private Map repository = new ConcurrentHashMap<>();
+ private AtomicInteger counter = new AtomicInteger(0);
+
+ @Override
+ public User save(User user) {
+ if (user.isNew()) {
+ user.setId(counter.incrementAndGet());
+ }
+ repository.put(user.getId(), user);
+ return user;
+ }
+
+ @Override
+ public boolean delete(int id) {
+ return repository.remove(id) != null;
+ }
+
+ @Override
+ public User get(int id) {
+ return repository.get(id);
+ }
+
+ @Override
+ public List getAll() {
+ return repository.values().stream()
+ .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ return repository.values().stream()
+ .filter(u -> email.equals(u.getEmail()))
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java
new file mode 100644
index 000000000000..22f5e92733a7
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java
@@ -0,0 +1,33 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.JpaUtil;
+
+import javax.validation.ConstraintViolationException;
+import java.util.Collections;
+import java.util.Date;
+
+abstract public class AbstractJpaUserServiceTest extends AbstractUserServiceTest {
+
+ @Autowired
+ private JpaUtil jpaUtil;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ jpaUtil.clear2ndLevelHibernateCache();
+ }
+
+ @Test
+ public void testValidation() throws Exception {
+ validateRootCause(() -> service.save(new User(null, " ", "invalid@yandex.ru", "password", Role.ROLE_USER)), ConstraintViolationException.class);
+ validateRootCause(() -> service.save(new User(null, "User", " ", "password", Role.ROLE_USER)), ConstraintViolationException.class);
+ validateRootCause(() -> service.save(new User(null, "User", "invalid@yandex.ru", " ", Role.ROLE_USER)), ConstraintViolationException.class);
+ validateRootCause(() -> service.save(new User(null, "User", "invalid@yandex.ru", "password", 9, true, new Date(), Collections.emptySet())), ConstraintViolationException.class);
+ validateRootCause(() -> service.save(new User(null, "User", "invalid@yandex.ru", "password", 10001, true, new Date(), Collections.emptySet())), ConstraintViolationException.class);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java
new file mode 100644
index 000000000000..421fe2324a5b
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java
@@ -0,0 +1,90 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import javax.validation.ConstraintViolationException;
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.Arrays;
+
+import static java.time.LocalDateTime.of;
+import static ru.javawebinar.topjava.MealTestData.*;
+import static ru.javawebinar.topjava.UserTestData.ADMIN_ID;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+
+public abstract class AbstractMealServiceTest extends AbstractServiceTest {
+
+ @Autowired
+ protected MealService service;
+
+ @Test
+ public void testDelete() throws Exception {
+ service.delete(MEAL1_ID, USER_ID);
+ MATCHER.assertCollectionEquals(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2), service.getAll(USER_ID));
+ }
+
+ @Test
+ public void testDeleteNotFound() throws Exception {
+ thrown.expect(NotFoundException.class);
+ service.delete(MEAL1_ID, 1);
+ }
+
+ @Test
+ public void testSave() throws Exception {
+ Meal created = getCreated();
+ service.save(created, USER_ID);
+ MATCHER.assertCollectionEquals(Arrays.asList(created, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1), service.getAll(USER_ID));
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID);
+ MATCHER.assertEquals(ADMIN_MEAL1, actual);
+ }
+
+ @Test
+ public void testGetNotFound() throws Exception {
+ thrown.expect(NotFoundException.class);
+ service.get(MEAL1_ID, ADMIN_ID);
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ Meal updated = getUpdated();
+ service.update(updated, USER_ID);
+ MATCHER.assertEquals(updated, service.get(MEAL1_ID, USER_ID));
+ }
+
+ @Test
+ public void testUpdateNotFound() throws Exception {
+ thrown.expect(NotFoundException.class);
+ thrown.expectMessage("Not found entity with id=" + MEAL1_ID);
+ service.update(MEAL1, ADMIN_ID);
+ }
+
+ @Test
+ public void testGetAll() throws Exception {
+ MATCHER.assertCollectionEquals(MEALS, service.getAll(USER_ID));
+ }
+
+ @Test
+ public void testGetBetween() throws Exception {
+ MATCHER.assertCollectionEquals(Arrays.asList(MEAL3, MEAL2, MEAL1),
+ service.getBetweenDates(
+ LocalDate.of(2015, Month.MAY, 30),
+ LocalDate.of(2015, Month.MAY, 30), USER_ID));
+ }
+
+ @Test
+ public void testValidation() throws Exception {
+ Assume.assumeTrue(isJpaBased());
+ validateRootCause(() -> service.save(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID), ConstraintViolationException.class);
+ validateRootCause(() -> service.save(new Meal(null, null, "Description", 300), USER_ID), ConstraintViolationException.class);
+ validateRootCause(() -> service.save(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID), ConstraintViolationException.class);
+ validateRootCause(() -> service.save(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 5001), USER_ID), ConstraintViolationException.class);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java
new file mode 100644
index 000000000000..beb58d2c8817
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java
@@ -0,0 +1,96 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.Stopwatch;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import ru.javawebinar.topjava.ActiveDbProfileResolver;
+import ru.javawebinar.topjava.Profiles;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+
+@ContextConfiguration({
+ "classpath:spring/spring-app.xml",
+ "classpath:spring/spring-db.xml"
+})
+@RunWith(SpringJUnit4ClassRunner.class)
+@ActiveProfiles(resolver = ActiveDbProfileResolver.class)
+@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8"))
+abstract public class AbstractServiceTest {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractServiceTest.class);
+
+ private static StringBuilder results = new StringBuilder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Autowired
+ public Environment env;
+
+ @Rule
+ // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev
+ public Stopwatch stopwatch = new Stopwatch() {
+ @Override
+ protected void finished(long nanos, Description description) {
+ String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos));
+ results.append(result).append('\n');
+ LOG.info(result + " ms\n");
+ }
+ };
+
+ static {
+ // needed only for java.util.logging (postgres driver)
+ SLF4JBridgeHandler.install();
+ }
+
+ public boolean isJpaBased() {
+// return Arrays.stream(env.getActiveProfiles()).noneMatch(Profiles.JDBC::equals);
+ return env.acceptsProfiles(Profiles.JPA, Profiles.DATAJPA);
+ }
+
+ @AfterClass
+ public static void printResult() {
+ LOG.info("\n---------------------------------" +
+ "\nTest Duration, ms" +
+ "\n---------------------------------\n" +
+ results +
+ "---------------------------------\n");
+ results.setLength(0);
+ }
+
+ // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778
+ public static void validateRootCause(Runnable runnable, Class exceptionClass) {
+ try {
+ runnable.run();
+ Assert.fail("Expected " + exceptionClass.getName());
+ } catch (Exception e) {
+ Assert.assertThat(getRootCause(e), instanceOf(exceptionClass));
+ }
+ }
+
+ // http://stackoverflow.com/a/28565320/548473
+ public static Throwable getRootCause(Throwable t) {
+ Throwable result = t;
+ Throwable cause;
+
+ while (null != (cause = result.getCause()) && (result != cause)) {
+ result = cause;
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java
new file mode 100644
index 000000000000..c4f7b3404ec5
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java
@@ -0,0 +1,84 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+
+import static ru.javawebinar.topjava.UserTestData.*;
+
+public abstract class AbstractUserServiceTest extends AbstractServiceTest {
+
+ @Autowired
+ protected UserService service;
+
+ @Before
+ public void setUp() throws Exception {
+ service.evictCache();
+ }
+
+ @Test
+ public void testSave() throws Exception {
+ User newUser = new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.ROLE_USER));
+ User created = service.save(newUser);
+ newUser.setId(created.getId());
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, newUser, USER), service.getAll());
+ }
+
+ @Test(expected = DataAccessException.class)
+ public void testDuplicateMailSave() throws Exception {
+ service.save(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.ROLE_USER));
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ service.delete(USER_ID);
+ MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), service.getAll());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testNotFoundDelete() throws Exception {
+ service.delete(1);
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ User user = service.get(ADMIN_ID);
+ MATCHER.assertEquals(ADMIN, user);
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetNotFound() throws Exception {
+ service.get(1);
+ }
+
+ @Test
+ public void testGetByEmail() throws Exception {
+ User user = service.getByEmail("admin@gmail.com");
+ MATCHER.assertEquals(ADMIN, user);
+ }
+
+ @Test
+ public void testGetAll() throws Exception {
+ Collection all = service.getAll();
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, USER), all);
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ User updated = new User(USER);
+ updated.setName("UpdatedName");
+ updated.setCaloriesPerDay(330);
+ updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN));
+ service.update(updated);
+ MATCHER.assertEquals(updated, service.get(USER_ID));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java
new file mode 100644
index 000000000000..0a76c1c9da1c
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java
@@ -0,0 +1,27 @@
+package ru.javawebinar.topjava.service.datajpa;
+
+import org.junit.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.service.AbstractMealServiceTest;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.MealTestData.*;
+import static ru.javawebinar.topjava.Profiles.DATAJPA;
+import static ru.javawebinar.topjava.UserTestData.ADMIN_ID;
+
+@ActiveProfiles(DATAJPA)
+public class DataJpaMealServiceTest extends AbstractMealServiceTest {
+ @Test
+ public void testGetWithUser() throws Exception {
+ Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID);
+ MATCHER.assertEquals(ADMIN_MEAL1, adminMeal);
+ UserTestData.MATCHER.assertEquals(UserTestData.ADMIN, adminMeal.getUser());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetWithUserNotFound() throws Exception {
+ service.getWithUser(MEAL1_ID, ADMIN_ID);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java
new file mode 100644
index 000000000000..160ea9572d92
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java
@@ -0,0 +1,26 @@
+package ru.javawebinar.topjava.service.datajpa;
+
+import org.junit.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.MealTestData;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.Profiles.DATAJPA;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+@ActiveProfiles(DATAJPA)
+public class DataJpaUserServiceTest extends AbstractJpaUserServiceTest {
+ @Test
+ public void testGetWithMeals() throws Exception {
+ User user = service.getWithMeals(USER_ID);
+ MATCHER.assertEquals(USER, user);
+ MealTestData.MATCHER.assertCollectionEquals(MealTestData.MEALS, user.getMeals());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetWithMealsNotFound() throws Exception {
+ service.getWithMeals(1);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java
new file mode 100644
index 000000000000..9ff4ae615b2b
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jdbc;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractMealServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JDBC;
+
+@ActiveProfiles(JDBC)
+public class JdbcMealServiceTest extends AbstractMealServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java
new file mode 100644
index 000000000000..419f68ed1098
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jdbc;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractUserServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JDBC;
+
+@ActiveProfiles(JDBC)
+public class JdbcUserServiceTest extends AbstractUserServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java
new file mode 100644
index 000000000000..70e7bf865421
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jpa;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractMealServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JPA;
+
+@ActiveProfiles(JPA)
+public class JpaMealServiceTest extends AbstractMealServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java
new file mode 100644
index 000000000000..ffb1fea03c22
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jpa;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JPA;
+
+@ActiveProfiles(JPA)
+public class JpaUserServiceTest extends AbstractJpaUserServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java
new file mode 100644
index 000000000000..b6a164133bbd
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java
@@ -0,0 +1,63 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.filter.CharacterEncodingFilter;
+import ru.javawebinar.topjava.AllActiveProfileResolver;
+import ru.javawebinar.topjava.repository.JpaUtil;
+import ru.javawebinar.topjava.service.UserService;
+
+import javax.annotation.PostConstruct;
+
+@ContextConfiguration({
+ "classpath:spring/spring-app.xml",
+ "classpath:spring/spring-mvc.xml",
+ "classpath:spring/spring-db.xml"
+})
+@WebAppConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+@Transactional
+@ActiveProfiles(resolver = AllActiveProfileResolver.class)
+abstract public class AbstractControllerTest {
+
+ private static final CharacterEncodingFilter CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter();
+
+ static {
+ CHARACTER_ENCODING_FILTER.setEncoding("UTF-8");
+ CHARACTER_ENCODING_FILTER.setForceEncoding(true);
+ }
+
+ @Autowired
+ private JpaUtil jpaUtil;
+
+ protected MockMvc mockMvc;
+
+ @Autowired
+ protected UserService userService;
+
+ @Autowired
+ private WebApplicationContext webApplicationContext;
+
+ @PostConstruct
+ private void postConstruct() {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(webApplicationContext)
+ .addFilter(CHARACTER_ENCODING_FILTER)
+ .build();
+ }
+
+ @Before
+ public void setUp() {
+ userService.evictCache();
+ jpaUtil.clear2ndLevelHibernateCache();
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerSpringTest.java
new file mode 100644
index 000000000000..42b03578127d
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerSpringTest.java
@@ -0,0 +1,50 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+import ru.javawebinar.topjava.web.user.AdminRestController;
+
+import java.util.Collection;
+
+import static ru.javawebinar.topjava.UserTestData.ADMIN;
+import static ru.javawebinar.topjava.UserTestData.USER;
+
+@ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/mock.xml"})
+@RunWith(SpringRunner.class)
+public class InMemoryAdminRestControllerSpringTest {
+
+ @Autowired
+ private AdminRestController controller;
+
+ @Autowired
+ private UserRepository repository;
+
+ @Before
+ public void setUp() throws Exception {
+ repository.getAll().forEach(u -> repository.delete(u.getId()));
+ repository.save(USER);
+ repository.save(ADMIN);
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ controller.delete(UserTestData.USER_ID);
+ Collection users = controller.getAll();
+ Assert.assertEquals(users.size(), 1);
+ Assert.assertEquals(users.iterator().next(), ADMIN);
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testDeleteNotFound() throws Exception {
+ controller.delete(10);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerTest.java
new file mode 100644
index 000000000000..6e5960f61f63
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerTest.java
@@ -0,0 +1,57 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.*;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+import ru.javawebinar.topjava.web.user.AdminRestController;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static ru.javawebinar.topjava.UserTestData.ADMIN;
+import static ru.javawebinar.topjava.UserTestData.USER;
+
+public class InMemoryAdminRestControllerTest {
+ private static ConfigurableApplicationContext appCtx;
+ private static AdminRestController controller;
+
+ @BeforeClass
+ public static void beforeClass() {
+ appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/mock.xml");
+ System.out.println("\n" + Arrays.toString(appCtx.getBeanDefinitionNames()) + "\n");
+ controller = appCtx.getBean(AdminRestController.class);
+ }
+
+ @AfterClass
+ public static void afterClass() {
+// May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed
+// http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite
+// appCtx.close();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ // Re-initialize
+ UserRepository repository = appCtx.getBean(UserRepository.class);
+ repository.getAll().forEach(u -> repository.delete(u.getId()));
+ repository.save(USER);
+ repository.save(ADMIN);
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ controller.delete(UserTestData.USER_ID);
+ Collection users = controller.getAll();
+ Assert.assertEquals(users.size(), 1);
+ Assert.assertEquals(users.iterator().next(), ADMIN);
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testDeleteNotFound() throws Exception {
+ controller.delete(10);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java
new file mode 100644
index 000000000000..dce3c4e3e3b2
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java
@@ -0,0 +1,29 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+import static ru.javawebinar.topjava.UserTestData.USER;
+import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ;
+
+public class RootControllerTest extends AbstractControllerTest {
+
+ @Test
+ public void testUsers() throws Exception {
+ mockMvc.perform(get("/users"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(view().name("users"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp"))
+ .andExpect(model().attribute("users", hasSize(2)))
+ .andExpect(model().attribute("users", hasItem(
+ allOf(
+ hasProperty("id", is(START_SEQ)),
+ hasProperty("name", is(USER.getName()))
+ )
+ )));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java
new file mode 100644
index 000000000000..1e2751cf92a9
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java
@@ -0,0 +1,26 @@
+package ru.javawebinar.topjava.web.json;
+
+import org.junit.Test;
+import ru.javawebinar.topjava.MealTestData;
+import ru.javawebinar.topjava.model.Meal;
+
+import java.util.List;
+
+public class JsonUtilTest {
+
+ @Test
+ public void testReadWriteValue() throws Exception {
+ String json = JsonUtil.writeValue(MealTestData.ADMIN_MEAL1);
+ System.out.println(json);
+ Meal meal = JsonUtil.readValue(json, Meal.class);
+ MealTestData.MATCHER.assertEquals(MealTestData.ADMIN_MEAL1, meal);
+ }
+
+ @Test
+ public void testReadWriteValues() throws Exception {
+ String json = JsonUtil.writeValue(MealTestData.MEALS);
+ System.out.println(json);
+ List meals = JsonUtil.readValues(json, Meal.class);
+ MealTestData.MATCHER.assertCollectionEquals(MealTestData.MEALS, meals);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java
new file mode 100644
index 000000000000..c95865ce5742
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java
@@ -0,0 +1,85 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.ResultActions;
+import ru.javawebinar.topjava.TestUtil;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+public class AdminRestControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL = AdminRestController.REST_URL + '/';
+
+ @Test
+ public void testGet() throws Exception {
+ mockMvc.perform(get(REST_URL + ADMIN_ID))
+ .andExpect(status().isOk())
+ .andDo(print())
+ // https://jira.spring.io/browse/SPR-14472
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentMatcher(ADMIN));
+ }
+
+ @Test
+ public void testGetByEmail() throws Exception {
+ mockMvc.perform(get(REST_URL + "by?email=" + USER.getEmail()))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentMatcher(USER));
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ mockMvc.perform(delete(REST_URL + USER_ID))
+ .andDo(print())
+ .andExpect(status().isOk());
+ MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), userService.getAll());
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ User updated = new User(USER);
+ updated.setName("UpdatedName");
+ updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN));
+ mockMvc.perform(put(REST_URL + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updated)))
+ .andExpect(status().isOk());
+
+ MATCHER.assertEquals(updated, userService.get(USER_ID));
+ }
+
+ @Test
+ public void testCreate() throws Exception {
+ User expected = new User(null, "New", "new@gmail.com", "newPass", Role.ROLE_USER, Role.ROLE_ADMIN);
+ ResultActions action = mockMvc.perform(post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(expected))).andExpect(status().isCreated());
+
+ User returned = MATCHER.fromJsonAction(action);
+ expected.setId(returned.getId());
+
+ MATCHER.assertEquals(expected, returned);
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, expected, USER), userService.getAll());
+ }
+
+ @Test
+ public void testGetAll() throws Exception {
+ TestUtil.print(mockMvc.perform(get(REST_URL))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentListMatcher(ADMIN, USER)));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java
new file mode 100644
index 000000000000..830de49dafaa
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java
@@ -0,0 +1,47 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+import ru.javawebinar.topjava.TestUtil;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.util.Collections;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javawebinar.topjava.UserTestData.*;
+import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL;
+
+public class ProfileRestControllerTest extends AbstractControllerTest {
+
+ @Test
+ public void testGet() throws Exception {
+ TestUtil.print(mockMvc.perform(get(REST_URL))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentMatcher(USER)));
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ mockMvc.perform(delete(REST_URL))
+ .andExpect(status().isOk());
+ MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), userService.getAll());
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ User updated = new User(USER_ID, "newName", "newemail@ya.ru", "newPassword", Role.ROLE_USER);
+ mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updated)))
+ .andDo(print())
+ .andExpect(status().isOk());
+
+ MATCHER.assertEquals(updated, new User(userService.getByEmail("newemail@ya.ru")));
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 000000000000..63c62b15dfe4
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,22 @@
+
+
+
+ true
+
+
+
+
+ UTF-8
+ %-5level %logger{0} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/resources/spring/mock.xml b/src/test/resources/spring/mock.xml
new file mode 100644
index 000000000000..66728caf0dfd
--- /dev/null
+++ b/src/test/resources/spring/mock.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file