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(AbstractBaseEntity entity) {
+ if (!entity.isNew()) {
+ throw new IllegalArgumentException(entity + " must be new (id=null)");
+ }
+ }
+
+ public static void assureIdConsistent(AbstractBaseEntity entity, int id) {
+// conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473)
+ if (entity.isNew()) {
+ entity.setId(id);
+ } else if (entity.id() != id) {
+ throw new IllegalArgumentException(entity + " must be with id=" + id);
+ }
+ }
+
+ // https://stackoverflow.com/a/65442410/548473
+ @NonNull
+ public static Throwable getRootCause(@NonNull Throwable t) {
+ Throwable rootCause = NestedExceptionUtils.getRootCause(t);
+ return rootCause != null ? rootCause : t;
+ }
+}
\ 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..921462ca98cd
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java
@@ -0,0 +1,54 @@
+package ru.javawebinar.topjava.web;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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.service.MealService;
+import ru.javawebinar.topjava.service.UserService;
+import ru.javawebinar.topjava.util.MealsUtil;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Controller
+public class RootController {
+ private static final Logger log = LoggerFactory.getLogger(RootController.class);
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private MealService mealService;
+
+ @GetMapping("/")
+ public String root() {
+ log.info("root");
+ return "index";
+ }
+
+ @GetMapping("/users")
+ public String getUsers(Model model) {
+ log.info("users");
+ model.addAttribute("users", userService.getAll());
+ return "users";
+ }
+
+ @PostMapping("/users")
+ public String setUser(HttpServletRequest request) {
+ int userId = Integer.parseInt(request.getParameter("userId"));
+ log.info("setUser {}", userId);
+ SecurityUtil.setAuthUserId(userId);
+ return "redirect:meals";
+ }
+
+ @GetMapping("/meals")
+ public String getMeals(Model model) {
+ log.info("meals");
+ model.addAttribute("meals",
+ MealsUtil.getTos(mealService.getAll(SecurityUtil.authUserId()), SecurityUtil.authUserCaloriesPerDay()));
+ return "meals";
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java
new file mode 100644
index 000000000000..4bad5863e3c6
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java
@@ -0,0 +1,25 @@
+package ru.javawebinar.topjava.web;
+
+import ru.javawebinar.topjava.model.AbstractBaseEntity;
+
+import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY;
+
+public class SecurityUtil {
+
+ private static int id = AbstractBaseEntity.START_SEQ;
+
+ private SecurityUtil() {
+ }
+
+ public static int authUserId() {
+ return id;
+ }
+
+ public static void setAuthUserId(int id) {
+ SecurityUtil.id = id;
+ }
+
+ public static int authUserCaloriesPerDay() {
+ return DEFAULT_CALORIES_PER_DAY;
+ }
+}
\ No newline at end of file
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..8237df93bffe
--- /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 https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers
+ */
+public class JacksonObjectMapper extends ObjectMapper {
+
+ private static final ObjectMapper MAPPER = new JacksonObjectMapper();
+
+ 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);
+ }
+
+ public static ObjectMapper getMapper() {
+ return MAPPER;
+ }
+}
\ 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..fda04590d618
--- /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);
+ }
+ }
+}
\ No newline at end of file
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..ec601c187896
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
@@ -0,0 +1,72 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.Nullable;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.service.MealService;
+import ru.javawebinar.topjava.to.MealTo;
+import ru.javawebinar.topjava.util.MealsUtil;
+import ru.javawebinar.topjava.web.SecurityUtil;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent;
+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 = SecurityUtil.authUserId();
+ log.info("get meal {} for user {}", id, userId);
+ return service.get(id, userId);
+ }
+
+ public void delete(int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("delete meal {} for user {}", id, userId);
+ service.delete(id, userId);
+ }
+
+ public List getAll() {
+ int userId = SecurityUtil.authUserId();
+ log.info("getAll for user {}", userId);
+ return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay());
+ }
+
+ public Meal create(Meal meal) {
+ int userId = SecurityUtil.authUserId();
+ log.info("create {} for user {}", meal, userId);
+ checkNew(meal);
+ return service.create(meal, userId);
+ }
+
+ public void update(Meal meal, int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("update {} for user {}", meal, userId);
+ assureIdConsistent(meal, id);
+ service.update(meal, userId);
+ }
+
+ /**
+ * Filter separately
+ * by date
+ * by time for every date
+ *
+ */
+ public List getBetween(@Nullable LocalDate startDate, @Nullable LocalTime startTime,
+ @Nullable LocalDate endDate, @Nullable LocalTime endTime) {
+ int userId = SecurityUtil.authUserId();
+ log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId);
+
+ List mealsDateFiltered = service.getBetweenInclusive(startDate, endDate, userId);
+ return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime);
+ }
+}
\ 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..7e800f683d8a
--- /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("/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 "mealForm";
+ }
+
+ @GetMapping("/create")
+ public String create(Model model) {
+ model.addAttribute("meal", new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000));
+ return "mealForm";
+ }
+
+ @PostMapping
+ public String updateOrCreate(HttpServletRequest request) {
+ Meal meal = new Meal(LocalDateTime.parse(request.getParameter("dateTime")),
+ request.getParameter("description"),
+ Integer.parseInt(request.getParameter("calories")));
+
+ if (request.getParameter("id").isEmpty()) {
+ super.create(meal);
+ } else {
+ super.update(meal, getId(request));
+ }
+ return "redirect:/meals";
+ }
+
+ @GetMapping("/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.parseInt(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..0000f1c1e02f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -0,0 +1,51 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNew;
+
+public abstract class AbstractUserController {
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ @Autowired
+ private UserService 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.create(user);
+ }
+
+ public void delete(int id) {
+ log.info("delete {}", id);
+ service.delete(id);
+ }
+
+ public void update(User user, int id) {
+ log.info("update {} with id={}", user, id);
+ assureIdConsistent(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..095ced3b0e1f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
@@ -0,0 +1,59 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.http.HttpStatus;
+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 java.net.URI;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = AdminRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class AdminRestController extends AbstractUserController {
+
+ static final String REST_URL = "/rest/admin/users";
+
+ @Override
+ @GetMapping
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @GetMapping("/{id}")
+ public User get(@PathVariable int id) {
+ return super.get(id);
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@RequestBody User user) {
+ User created = super.create(user);
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL + "/{id}")
+ .buildAndExpand(created.getId()).toUri();
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @Override
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable int id) {
+ super.delete(id);
+ }
+
+ @Override
+ @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void update(@RequestBody User user, @PathVariable int id) {
+ super.update(user, id);
+ }
+
+ @Override
+ @GetMapping("/by-email")
+ public User getByMail(@RequestParam 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..14559e4cf6fd
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,36 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.User;
+
+import static ru.javawebinar.topjava.web.SecurityUtil.authUserId;
+
+@RestController
+@RequestMapping(value = ProfileRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class ProfileRestController extends AbstractUserController {
+ static final String REST_URL = "/rest/profile";
+
+ @GetMapping
+ public User get() {
+ return super.get(authUserId());
+ }
+
+ @DeleteMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete() {
+ super.delete(authUserId());
+ }
+
+ @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void update(@RequestBody User user) {
+ super.update(user, authUserId());
+ }
+
+ @GetMapping("/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..05589f71f06e
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+ 5
+
+ 5000
+
+
+
+
+
+
+ 1
+
+
+
+
diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties
new file mode 100644
index 000000000000..17c03ef4ebda
--- /dev/null
+++ b/src/main/resources/db/hsqldb.properties
@@ -0,0 +1,11 @@
+#database.url=jdbc:hsqldb:file:D:/temp/topjava
+
+database.url=jdbc:hsqldb:mem:topjava
+database.username=sa
+database.password=
+
+database.init=true
+jdbc.initLocation=classpath:db/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..4bf3d8446ac5
--- /dev/null
+++ b/src/main/resources/db/initDB.sql
@@ -0,0 +1,37 @@
+DROP TABLE IF EXISTS user_role;
+DROP TABLE IF EXISTS meal;
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS global_seq;
+
+CREATE SEQUENCE global_seq START WITH 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() NOT NULL,
+ enabled BOOL DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
+
+CREATE TABLE user_role
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR NOT NULL,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meal
+(
+ 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 meal_unique_user_datetime_idx ON meal (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..9e0e195e600b
--- /dev/null
+++ b/src/main/resources/db/initDB_hsql.sql
@@ -0,0 +1,39 @@
+DROP TABLE user_role IF EXISTS;
+DROP TABLE meal 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) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ registered TIMESTAMP DEFAULT now() NOT NULL,
+ enabled BOOLEAN DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx
+ ON USERS (email);
+
+CREATE TABLE user_role
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR(255) NOT NULL,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meal
+(
+ 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 meal_unique_user_datetime_idx
+ ON meal (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..9e9bd828babe
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,25 @@
+DELETE FROM user_role;
+DELETE FROM meal;
+DELETE FROM users;
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+INSERT INTO users (name, email, password)
+VALUES ('User', 'user@yandex.ru', 'password'),
+ ('Admin', 'admin@gmail.com', 'admin'),
+ ('Guest', 'guest@gmail.com', 'guest');
+
+INSERT INTO user_role (role, user_id)
+VALUES ('USER', 100000),
+ ('ADMIN', 100001),
+ ('USER', 100001);
+
+INSERT INTO meal (date_time, description, calories, user_id)
+VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000),
+ ('2020-01-30 13:00:00', 'Обед', 1000, 100000),
+ ('2020-01-30 20:00:00', 'Ужин', 500, 100000),
+ ('2020-01-31 0:00:00', 'Еда на граничное значение', 100, 100000),
+ ('2020-01-31 10:00:00', 'Завтрак', 500, 100000),
+ ('2020-01-31 13:00:00', 'Обед', 1000, 100000),
+ ('2020-01-31 20:00:00', 'Ужин', 510, 100000),
+ ('2020-01-31 14:00:00', 'Админ ланч', 510, 100001),
+ ('2020-01-31 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..c56854a9b452
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,10 @@
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=user
+database.password=password
+
+database.init=true
+jdbc.initLocation=classpath:db/initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+#https://hibernate.atlassian.net/browse/HHH-13280
+hibernate.use_sql_comments=false
\ 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..2e073681ad16
--- /dev/null
+++ b/src/main/resources/db/tomcat.properties
@@ -0,0 +1,5 @@
+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..1d27e3f1ea2b
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ ${TOPJAVA_ROOT}/log/topjava.log
+
+
+ UTF-8
+ %date %-5level %logger{50}.%M:%L - %msg%n
+
+
+
+
+
+ UTF-8
+ %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml
new file mode 100644
index 000000000000..d57b656aeb75
--- /dev/null
+++ b/src/main/resources/spring/spring-app.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-cache.xml b/src/main/resources/spring/spring-cache.xml
new file mode 100644
index 000000000000..73325fee065f
--- /dev/null
+++ b/src/main/resources/spring/spring-cache.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..48afdb11a749
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..68fe83a4063a
--- /dev/null
+++ b/src/main/resources/spring/spring-mvc.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ text/plain;charset=UTF-8
+ text/html;charset=UTF-8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..5b5efe57e65b
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp
@@ -0,0 +1,6 @@
+<%@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..84719196538d
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/index.jsp
@@ -0,0 +1,21 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ 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/mealForm.jsp b/src/main/webapp/WEB-INF/jsp/mealForm.jsp
new file mode 100644
index 000000000000..af6d7880ec98
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/mealForm.jsp
@@ -0,0 +1,35 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
+
+
+
+
+
+
+<%-- `meal.new` cause javax.el.ELException - bug tomcat --%>
+
+
+
+
+
+
+
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..b42230b902bd
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/meals.jsp
@@ -0,0 +1,64 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ 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:replace(meal.dateTime, 'T', ' ')}--%>
+ ${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..4d3d8678906c
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/users.jsp
@@ -0,0 +1,38 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ 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.enabled}
+
+
+
+
+
+
+
+
\ No newline at end of file
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..65af7c831300
--- /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..a55147510899
--- /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;
+}
+
+tr[data-meal-excess="false"] {
+ color: green;
+}
+
+tr[data-meal-excess="true"] {
+ color: red;
+}
+
+header, footer {
+ background: none repeat scroll 0 0 #A6C9E2;
+ color: #2E6E9E;
+ font-size: 20px;
+ padding: 5px 20px;
+ margin: 6px 0;
+}
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..43f143cc7664
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java
@@ -0,0 +1,19 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.lang.NonNull;
+import org.springframework.test.context.support.DefaultActiveProfilesResolver;
+
+import java.util.Arrays;
+
+//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver
+public class ActiveDbProfileResolver extends DefaultActiveProfilesResolver {
+ @Override
+ public @NonNull
+ String[] resolve(@NonNull Class> aClass) {
+ // https://stackoverflow.com/a/52438829/548473
+ String[] activeProfiles = super.resolve(aClass);
+ String[] activeProfilesWithDb = Arrays.copyOf(activeProfiles, activeProfiles.length + 1);
+ activeProfilesWithDb[activeProfiles.length] = Profiles.getActiveDbProfile();
+ return activeProfilesWithDb;
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java
new file mode 100644
index 000000000000..40f3d8c22b77
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java
@@ -0,0 +1,67 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Factory for creating test matchers.
+ *
+ * Comparing actual and expected objects via AssertJ
+ * Support converting json MvcResult to objects for comparation.
+ */
+public class MatcherFactory {
+ public static Matcher usingIgnoringFieldsComparator(Class clazz, String... fieldsToIgnore) {
+ return new Matcher<>(clazz, fieldsToIgnore);
+ }
+
+ public static class Matcher {
+ private final Class clazz;
+ private final String[] fieldsToIgnore;
+
+ private Matcher(Class clazz, String... fieldsToIgnore) {
+ this.clazz = clazz;
+ this.fieldsToIgnore = fieldsToIgnore;
+ }
+
+ public void assertMatch(T actual, T expected) {
+ assertThat(actual).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(expected);
+ }
+
+ @SafeVarargs
+ public final void assertMatch(Iterable actual, T... expected) {
+ assertMatch(actual, List.of(expected));
+ }
+
+ public void assertMatch(Iterable actual, Iterable expected) {
+ assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(expected);
+ }
+
+ public ResultMatcher contentJson(T expected) {
+ return result -> assertMatch(JsonUtil.readValue(getContent(result), clazz), expected);
+ }
+
+ @SafeVarargs
+ public final ResultMatcher contentJson(T... expected) {
+ return contentJson(List.of(expected));
+ }
+
+ public ResultMatcher contentJson(Iterable expected) {
+ return result -> assertMatch(JsonUtil.readValues(getContent(result), clazz), expected);
+ }
+
+ public T readFromJson(ResultActions action) throws UnsupportedEncodingException {
+ return JsonUtil.readValue(getContent(action.andReturn()), clazz);
+ }
+
+ private static String getContent(MvcResult result) throws UnsupportedEncodingException {
+ return result.getResponse().getContentAsString();
+ }
+ }
+}
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..c2697db04001
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java
@@ -0,0 +1,38 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.model.Meal;
+
+import java.time.Month;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+
+import static java.time.LocalDateTime.of;
+import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ;
+
+public class MealTestData {
+ public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(Meal.class, "user");
+
+ public static final int NOT_FOUND = 10;
+ public static final int MEAL1_ID = START_SEQ + 3;
+ public static final int ADMIN_MEAL_ID = START_SEQ + 10;
+
+ public static final Meal meal1 = new Meal(MEAL1_ID, of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500);
+ public static final Meal meal2 = new Meal(MEAL1_ID + 1, of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000);
+ public static final Meal meal3 = new Meal(MEAL1_ID + 2, of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500);
+ public static final Meal meal4 = new Meal(MEAL1_ID + 3, of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100);
+ public static final Meal meal5 = new Meal(MEAL1_ID + 4, of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 500);
+ public static final Meal meal6 = new Meal(MEAL1_ID + 5, of(2020, Month.JANUARY, 31, 13, 0), "Обед", 1000);
+ public static final Meal meal7 = new Meal(MEAL1_ID + 6, of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 510);
+ public static final Meal adminMeal1 = new Meal(ADMIN_MEAL_ID, of(2020, Month.JANUARY, 31, 14, 0), "Админ ланч", 510);
+ public static final Meal adminMeal2 = new Meal(ADMIN_MEAL_ID + 1, of(2020, Month.JANUARY, 31, 21, 0), "Админ ужин", 1500);
+
+ public static final List meals = List.of(meal7, meal6, meal5, meal4, meal3, meal2, meal1);
+
+ public static Meal getNew() {
+ return new Meal(null, of(2020, Month.FEBRUARY, 1, 18, 0), "Созданный ужин", 300);
+ }
+
+ public static Meal getUpdated() {
+ return new Meal(MEAL1_ID, meal1.getDateTime().plus(2, ChronoUnit.MINUTES), "Обновленный завтрак", 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..1dda999c543a
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java
@@ -0,0 +1,36 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.MealTo;
+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 (ARM)
+ try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/inmemory.xml")) {
+ System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames()));
+ AdminRestController adminUserController = appCtx.getBean(AdminRestController.class);
+ adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ADMIN));
+ System.out.println();
+
+ MealRestController mealController = appCtx.getBean(MealRestController.class);
+ List filteredMealsWithExcess =
+ mealController.getBetween(
+ LocalDate.of(2020, Month.JANUARY, 30), LocalTime.of(7, 0),
+ LocalDate.of(2020, Month.JANUARY, 31), LocalTime.of(11, 0));
+ filteredMealsWithExcess.forEach(System.out::println);
+ System.out.println();
+ System.out.println(mealController.getBetween(null, null, null, null));
+ }
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/TimingExtension.java b/src/test/java/ru/javawebinar/topjava/TimingExtension.java
new file mode 100644
index 000000000000..cee6ae92c6b8
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/TimingExtension.java
@@ -0,0 +1,36 @@
+package ru.javawebinar.topjava;
+
+import org.junit.jupiter.api.extension.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.StopWatch;
+
+public class TimingExtension implements
+ BeforeTestExecutionCallback, AfterTestExecutionCallback, BeforeAllCallback, AfterAllCallback {
+
+ private static final Logger log = LoggerFactory.getLogger("result");
+
+ private StopWatch stopWatch;
+
+ @Override
+ public void beforeAll(ExtensionContext extensionContext) {
+ stopWatch = new StopWatch("Execution time of " + extensionContext.getRequiredTestClass().getSimpleName());
+ }
+
+ @Override
+ public void beforeTestExecution(ExtensionContext extensionContext) {
+ String testName = extensionContext.getDisplayName();
+ log.info("\nStart " + testName);
+ stopWatch.start(testName);
+ }
+
+ @Override
+ public void afterTestExecution(ExtensionContext extensionContext) {
+ stopWatch.stop();
+ }
+
+ @Override
+ public void afterAll(ExtensionContext extensionContext) {
+ log.info('\n' + stopWatch.prettyPrint() + '\n');
+ }
+}
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..2859645521f0
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+
+import java.util.Collections;
+import java.util.Date;
+
+import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ;
+
+public class UserTestData {
+ public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(User.class, "registered", "meals");
+
+ public static final int USER_ID = START_SEQ;
+ public static final int ADMIN_ID = START_SEQ + 1;
+ public static final int GUEST_ID = START_SEQ + 2;
+ public static final int NOT_FOUND = 10;
+
+ public static final User user = new User(USER_ID, "User", "user@yandex.ru", "password", Role.USER);
+ public static final User admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ADMIN, Role.USER);
+ public static final User guest = new User(GUEST_ID, "Guest", "guest@gmail.com", "guest");
+
+ public static User getNew() {
+ return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.USER));
+ }
+
+ public static User getUpdated() {
+ User updated = new User(user);
+ updated.setEmail("update@gmail.com");
+ updated.setName("UpdatedName");
+ updated.setCaloriesPerDay(330);
+ updated.setPassword("newPass");
+ updated.setEnabled(false);
+ updated.setRoles(Collections.singletonList(Role.ADMIN));
+ return updated;
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java
new file mode 100644
index 000000000000..03770da2134b
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java
@@ -0,0 +1,45 @@
+package ru.javawebinar.topjava.repository.inmemory;
+
+import ru.javawebinar.topjava.model.AbstractBaseEntity;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ;
+
+public class InMemoryBaseRepository {
+
+ static final AtomicInteger counter = new AtomicInteger(START_SEQ);
+
+ final Map map = new ConcurrentHashMap<>();
+
+ public T save(T entity) {
+ Objects.requireNonNull(entity, "Entity must not be null");
+ if (entity.isNew()) {
+ entity.setId(counter.incrementAndGet());
+ map.put(entity.getId(), entity);
+ return entity;
+ }
+ return map.computeIfPresent(entity.getId(), (id, oldT) -> entity);
+ }
+
+ public boolean delete(int id) {
+ return map.remove(id) != null;
+ }
+
+ public T get(int id) {
+ return map.get(id);
+ }
+
+ Collection getCollection() {
+ return map.values();
+ }
+
+ void put(T entity) {
+ Objects.requireNonNull(entity, "Entity must not be null");
+ map.put(entity.id(), entity);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java
new file mode 100644
index 000000000000..5c65ced864e4
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java
@@ -0,0 +1,80 @@
+package ru.javawebinar.topjava.repository.inmemory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.MealTestData;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+import ru.javawebinar.topjava.util.Util;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Predicate;
+
+@Repository
+public class InMemoryMealRepository implements MealRepository {
+ private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepository.class);
+
+ // Map userId -> mealRepository
+ private final Map> usersMealsMap = new ConcurrentHashMap<>();
+
+ {
+ var userMeals = new InMemoryBaseRepository();
+ MealTestData.meals.forEach(userMeals::put);
+ usersMealsMap.put(UserTestData.USER_ID, userMeals);
+ }
+
+
+ @Override
+ public Meal save(Meal meal, int userId) {
+ Objects.requireNonNull(meal, "meal must not be null");
+ var meals = usersMealsMap.computeIfAbsent(userId, uId -> new InMemoryBaseRepository<>());
+ return meals.save(meal);
+ }
+
+ @PostConstruct
+ public void postConstruct() {
+ log.info("+++ PostConstruct");
+ }
+
+ @PreDestroy
+ public void preDestroy() {
+ log.info("+++ PreDestroy");
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ var meals = usersMealsMap.get(userId);
+ return meals != null && meals.delete(id);
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ var meals = usersMealsMap.get(userId);
+ return meals == null ? null : meals.get(id);
+ }
+
+ @Override
+ public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ return filterByPredicate(userId, meal -> Util.isBetweenHalfOpen(meal.getDateTime(), startDateTime, endDateTime));
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return filterByPredicate(userId, meal -> true);
+ }
+
+ private List filterByPredicate(int userId, Predicate filter) {
+ var meals = usersMealsMap.get(userId);
+ return meals == null ? Collections.emptyList() :
+ meals.getCollection().stream()
+ .filter(filter)
+ .sorted(Comparator.comparing(Meal::getDateTime).reversed())
+ .toList();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java
new file mode 100644
index 000000000000..f3585dfffd7f
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java
@@ -0,0 +1,40 @@
+package ru.javawebinar.topjava.repository.inmemory;
+
+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.Objects;
+
+import static ru.javawebinar.topjava.UserTestData.*;
+
+
+@Repository
+public class InMemoryUserRepository extends InMemoryBaseRepository implements UserRepository {
+
+ public void init() {
+ map.clear();
+ put(user);
+ put(admin);
+ put(guest);
+ counter.getAndSet(GUEST_ID + 1);
+ }
+
+ @Override
+ public List getAll() {
+ return getCollection().stream()
+ .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail))
+ .toList();
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ Objects.requireNonNull(email, "email must not be null");
+ return getCollection().stream()
+ .filter(u -> email.equals(u.getEmail()))
+ .findFirst()
+ .orElse(null);
+ }
+}
\ 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..6b00e448152b
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java
@@ -0,0 +1,112 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+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 static java.time.LocalDateTime.of;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+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
+ void delete() {
+ service.delete(MEAL1_ID, USER_ID);
+ assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, USER_ID));
+ }
+
+ @Test
+ void deleteNotFound() {
+ assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND, USER_ID));
+ }
+
+ @Test
+ void deleteNotOwn() {
+ assertThrows(NotFoundException.class, () -> service.delete(MEAL1_ID, ADMIN_ID));
+ }
+
+ @Test
+ void create() {
+ Meal created = service.create(getNew(), USER_ID);
+ int newId = created.id();
+ Meal newMeal = getNew();
+ newMeal.setId(newId);
+ MEAL_MATCHER.assertMatch(created, newMeal);
+ MEAL_MATCHER.assertMatch(service.get(newId, USER_ID), newMeal);
+ }
+
+ @Test
+ void duplicateDateTimeCreate() {
+ assertThrows(DataAccessException.class, () ->
+ service.create(new Meal(null, meal1.getDateTime(), "duplicate", 100), USER_ID));
+ }
+
+ @Test
+ void get() {
+ Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID);
+ MEAL_MATCHER.assertMatch(actual, adminMeal1);
+ }
+
+ @Test
+ void getNotFound() {
+ assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND, USER_ID));
+ }
+
+ @Test
+ void getNotOwn() {
+ assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, ADMIN_ID));
+ }
+
+ @Test
+ void update() {
+ Meal updated = getUpdated();
+ service.update(updated, USER_ID);
+ MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated());
+ }
+
+ @Test
+ void updateNotOwn() {
+ NotFoundException exception = assertThrows(NotFoundException.class, () -> service.update(getUpdated(), ADMIN_ID));
+ Assertions.assertEquals("Not found entity with id=" + MEAL1_ID, exception.getMessage());
+ MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), meal1);
+ }
+
+ @Test
+ void getAll() {
+ MEAL_MATCHER.assertMatch(service.getAll(USER_ID), meals);
+ }
+
+ @Test
+ void getBetweenInclusive() {
+ MEAL_MATCHER.assertMatch(service.getBetweenInclusive(
+ LocalDate.of(2020, Month.JANUARY, 30),
+ LocalDate.of(2020, Month.JANUARY, 30), USER_ID),
+ meal3, meal2, meal1);
+ }
+
+ @Test
+ void getBetweenWithNullDates() {
+ MEAL_MATCHER.assertMatch(service.getBetweenInclusive(null, null, USER_ID), meals);
+ }
+
+ @Test
+ void createWithException() throws Exception {
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID));
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, null, "Description", 300), USER_ID));
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID));
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 5001), USER_ID));
+ }
+}
\ 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..06f72ef86ac8
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java
@@ -0,0 +1,34 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import ru.javawebinar.topjava.ActiveDbProfileResolver;
+import ru.javawebinar.topjava.TimingExtension;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause;
+
+@SpringJUnitConfig(locations = {
+ "classpath:spring/spring-app.xml",
+ "classpath:spring/spring-db.xml"
+})
+//@ExtendWith(SpringExtension.class)
+@ActiveProfiles(resolver = ActiveDbProfileResolver.class)
+@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8"))
+@ExtendWith(TimingExtension.class)
+public abstract class AbstractServiceTest {
+
+ // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778
+ protected void validateRootCause(Class rootExceptionClass, Runnable runnable) {
+ assertThrows(rootExceptionClass, () -> {
+ try {
+ runnable.run();
+ } catch (Exception e) {
+ throw getRootCause(e);
+ }
+ });
+ }
+}
\ 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..fe6838b5f26f
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java
@@ -0,0 +1,88 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.jupiter.api.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 javax.validation.ConstraintViolationException;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+public abstract class AbstractUserServiceTest extends AbstractServiceTest {
+
+ @Autowired
+ protected UserService service;
+
+ @Test
+ void create() {
+ User created = service.create(getNew());
+ int newId = created.id();
+ User newUser = getNew();
+ newUser.setId(newId);
+ USER_MATCHER.assertMatch(created, newUser);
+ USER_MATCHER.assertMatch(service.get(newId), newUser);
+ }
+
+ @Test
+ void duplicateMailCreate() {
+ assertThrows(DataAccessException.class, () ->
+ service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.USER)));
+ }
+
+ @Test
+ void delete() {
+ service.delete(USER_ID);
+ assertThrows(NotFoundException.class, () -> service.get(USER_ID));
+ }
+
+ @Test
+ void deletedNotFound() {
+ assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND));
+ }
+
+ @Test
+ void get() {
+ User user = service.get(ADMIN_ID);
+ USER_MATCHER.assertMatch(user, admin);
+ }
+
+ @Test
+ void getNotFound() {
+ assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND));
+ }
+
+ @Test
+ void getByEmail() {
+ User user = service.getByEmail("admin@gmail.com");
+ USER_MATCHER.assertMatch(user, admin);
+ }
+
+ @Test
+ void update() {
+ User updated = getUpdated();
+ service.update(updated);
+ USER_MATCHER.assertMatch(service.get(USER_ID), getUpdated());
+ }
+
+ @Test
+ void getAll() {
+ List all = service.getAll();
+ USER_MATCHER.assertMatch(all, admin, guest, user);
+ }
+
+ @Test
+ void createWithException() throws Exception {
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.USER)));
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", Role.USER)));
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.USER)));
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Set.of())));
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Set.of())));
+ }
+}
\ 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..161c93fb5296
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java
@@ -0,0 +1,29 @@
+package ru.javawebinar.topjava.service.datajpa;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.MealTestData;
+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.*;
+
+@ActiveProfiles(DATAJPA)
+class DataJpaMealServiceTest extends AbstractMealServiceTest {
+ @Test
+ void getWithUser() {
+ Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID);
+ MEAL_MATCHER.assertMatch(adminMeal, adminMeal1);
+ USER_MATCHER.assertMatch(adminMeal.getUser(), admin);
+ }
+
+ @Test
+ void getWithUserNotFound() {
+ Assertions.assertThrows(NotFoundException.class,
+ () -> service.getWithUser(MealTestData.NOT_FOUND, 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..d8a1f4106c1f
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java
@@ -0,0 +1,30 @@
+package ru.javawebinar.topjava.service.datajpa;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.MealTestData;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.AbstractUserServiceTest;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.MealTestData.MEAL_MATCHER;
+import static ru.javawebinar.topjava.Profiles.DATAJPA;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+@ActiveProfiles(DATAJPA)
+class DataJpaUserServiceTest extends AbstractUserServiceTest {
+ @Test
+ void getWithMeals() {
+ User admin = service.getWithMeals(ADMIN_ID);
+ USER_MATCHER.assertMatch(admin, UserTestData.admin);
+ MEAL_MATCHER.assertMatch(admin.getMeals(), MealTestData.adminMeal2, MealTestData.adminMeal1);
+ }
+
+ @Test
+ void getWithMealsNotFound() {
+ Assertions.assertThrows(NotFoundException.class,
+ () -> service.getWithMeals(NOT_FOUND));
+ }
+}
\ 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..aef588264f71
--- /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)
+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..62ca7668cf67
--- /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)
+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..aaf5dcda960e
--- /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)
+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..6d1cd91543fc
--- /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.AbstractUserServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JPA;
+
+@ActiveProfiles(JPA)
+class JpaUserServiceTest extends AbstractUserServiceTest {
+}
\ 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..6fa9b1d4c81b
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java
@@ -0,0 +1,52 @@
+package ru.javawebinar.topjava.web;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+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.ActiveDbProfileResolver;
+import ru.javawebinar.topjava.Profiles;
+
+import javax.annotation.PostConstruct;
+
+@SpringJUnitWebConfig(locations = {
+ "classpath:spring/spring-app.xml",
+ "classpath:spring/spring-mvc.xml",
+ "classpath:spring/spring-db.xml"
+})
+//@WebAppConfiguration
+//@ExtendWith(SpringExtension.class)
+@Transactional
+@ActiveProfiles(resolver = ActiveDbProfileResolver.class, profiles = Profiles.REPOSITORY_IMPLEMENTATION)
+public abstract class AbstractControllerTest {
+
+ private static final CharacterEncodingFilter CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter();
+
+ static {
+ CHARACTER_ENCODING_FILTER.setEncoding("UTF-8");
+ CHARACTER_ENCODING_FILTER.setForceEncoding(true);
+ }
+
+ private MockMvc mockMvc;
+
+ @Autowired
+ private WebApplicationContext webApplicationContext;
+
+ @PostConstruct
+ private void postConstruct() {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(webApplicationContext)
+ .addFilter(CHARACTER_ENCODING_FILTER)
+ .build();
+ }
+
+ protected ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception {
+ return mockMvc.perform(builder);
+ }
+}
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..2bec37dda60e
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java
@@ -0,0 +1,32 @@
+package ru.javawebinar.topjava.web;
+
+import org.assertj.core.matcher.AssertionMatcher;
+import org.junit.jupiter.api.Test;
+import ru.javawebinar.topjava.model.User;
+
+import java.util.List;
+
+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.*;
+
+class RootControllerTest extends AbstractControllerTest {
+
+ @Test
+ void getUsers() throws Exception {
+ perform(get("/users"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(view().name("users"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp"))
+ .andExpect(model().attribute("users",
+ new AssertionMatcher>() {
+ @Override
+ public void assertion(List actual) throws AssertionError {
+ USER_MATCHER.assertMatch(actual, admin, guest, user);
+ }
+ }
+ ));
+ }
+}
\ 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..540586d114fd
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java
@@ -0,0 +1,30 @@
+package ru.javawebinar.topjava.web.json;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ru.javawebinar.topjava.model.Meal;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.MealTestData.*;
+
+class JsonUtilTest {
+ private static final Logger log = LoggerFactory.getLogger(JsonUtilTest.class);
+
+ @Test
+ void readWriteValue() {
+ String json = JsonUtil.writeValue(adminMeal1);
+ log.info(json);
+ Meal meal = JsonUtil.readValue(json, Meal.class);
+ MEAL_MATCHER.assertMatch(meal, adminMeal1);
+ }
+
+ @Test
+ void readWriteValues() {
+ String json = JsonUtil.writeValue(meals);
+ log.info(json);
+ List actual = JsonUtil.readValues(json, Meal.class);
+ MEAL_MATCHER.assertMatch(actual, 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..6af80902c749
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java
@@ -0,0 +1,86 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+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.*;
+
+class AdminRestControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL = AdminRestController.REST_URL + '/';
+
+ @Autowired
+ private UserService userService;
+
+ @Test
+ void get() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID))
+ .andExpect(status().isOk())
+ .andDo(print())
+ // https://jira.spring.io/browse/SPR-14472
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(admin));
+ }
+
+ @Test
+ void getByEmail() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + "by-email?email=" + user.getEmail()))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(user));
+ }
+
+ @Test
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL + USER_ID))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+ assertThrows(NotFoundException.class, () -> userService.get(USER_ID));
+ }
+
+ @Test
+ void update() throws Exception {
+ User updated = getUpdated();
+ perform(MockMvcRequestBuilders.put(REST_URL + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updated)))
+ .andExpect(status().isNoContent());
+
+ USER_MATCHER.assertMatch(userService.get(USER_ID), updated);
+ }
+
+ @Test
+ void createWithLocation() throws Exception {
+ User newUser = getNew();
+ ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(newUser)))
+ .andExpect(status().isCreated());
+
+ User created = USER_MATCHER.readFromJson(action);
+ int newId = created.id();
+ newUser.setId(newId);
+ USER_MATCHER.assertMatch(created, newUser);
+ USER_MATCHER.assertMatch(userService.get(newId), newUser);
+ }
+
+ @Test
+ void getAll() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(admin, guest, user));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java
new file mode 100644
index 000000000000..7568d0f52491
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java
@@ -0,0 +1,38 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.UserTestData.NOT_FOUND;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+
+@SpringJUnitConfig(locations = {"classpath:spring/inmemory.xml"})
+class InMemoryAdminRestControllerSpringTest {
+
+ @Autowired
+ private AdminRestController controller;
+
+ @Autowired
+ private InMemoryUserRepository repository;
+
+ @BeforeEach
+ void setup() {
+ repository.init();
+ }
+
+ @Test
+ void delete() {
+ controller.delete(USER_ID);
+ Assertions.assertNull(repository.get(USER_ID));
+ }
+
+ @Test
+ void deleteNotFound() {
+ Assertions.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java
new file mode 100644
index 000000000000..c41fa0e6bb4d
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java
@@ -0,0 +1,54 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.jupiter.api.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.util.Arrays;
+
+import static ru.javawebinar.topjava.UserTestData.NOT_FOUND;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+
+class InMemoryAdminRestControllerTest {
+ private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class);
+
+ private static ConfigurableApplicationContext appCtx;
+ private static AdminRestController controller;
+ private static InMemoryUserRepository repository;
+
+ @BeforeAll
+ static void beforeClass() {
+ appCtx = new ClassPathXmlApplicationContext("spring/inmemory.xml");
+ log.info("\n{}\n", Arrays.toString(appCtx.getBeanDefinitionNames()));
+ controller = appCtx.getBean(AdminRestController.class);
+ repository = appCtx.getBean(InMemoryUserRepository.class);
+ }
+
+ @AfterAll
+ 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();
+ }
+
+ @BeforeEach
+ void setup() {
+ // re-initialize
+ repository.init();
+ }
+
+ @Test
+ void delete() {
+ controller.delete(USER_ID);
+ Assertions.assertNull(repository.get(USER_ID));
+ }
+
+ @Test
+ void deleteNotFound() {
+ Assertions.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND));
+ }
+}
\ 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..e8882742da6d
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java
@@ -0,0 +1,48 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+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;
+
+class ProfileRestControllerTest extends AbstractControllerTest {
+
+ @Autowired
+ private UserService userService;
+
+ @Test
+ void get() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(user));
+ }
+
+ @Test
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL))
+ .andExpect(status().isNoContent());
+ USER_MATCHER.assertMatch(userService.getAll(), admin, guest);
+ }
+
+ @Test
+ void update() throws Exception {
+ User updated = getUpdated();
+ perform(MockMvcRequestBuilders.put(REST_URL).contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updated)))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+
+ USER_MATCHER.assertMatch(userService.get(USER_ID), updated);
+ }
+}
\ 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..803655475302
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,32 @@
+
+
+
+ true
+
+
+
+
+ UTF-8
+ %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n
+
+
+
+
+
+ UTF-8
+ %magenta(%msg%n)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml
new file mode 100644
index 000000000000..0c9d0502857d
--- /dev/null
+++ b/src/test/resources/spring/inmemory.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/resources/spring/spring-cache.xml b/src/test/resources/spring/spring-cache.xml
new file mode 100644
index 000000000000..7c9dfda9a9f3
--- /dev/null
+++ b/src/test/resources/spring/spring-cache.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
\ No newline at end of file