{
+
+ @Override
+ public LocalTime parse(String text, Locale locale) {
+ return LocalTime.parse(text);
+ }
+
+ @Override
+ public String print(LocalTime lt, Locale locale) {
+ return lt.format(DateTimeFormatter.ISO_LOCAL_TIME);
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java
new file mode 100644
index 000000000..4ee6eaef1
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java
@@ -0,0 +1,25 @@
+package ru.javawebinar.topjava.web.interceptor;
+
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.web.SecurityUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This interceptor adds userTo to the model of every requests
+ */
+public class ModelInterceptor implements HandlerInterceptor {
+
+ @Override
+ public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+ if (modelAndView != null && !modelAndView.isEmpty()) {
+ AuthorizedUser authorizedUser = SecurityUtil.safeGet();
+ if (authorizedUser != null) {
+ modelAndView.getModelMap().addAttribute("userTo", authorizedUser.getUserTo());
+ }
+ }
+ }
+}
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 000000000..8237df93b
--- /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 000000000..d3bf1ac04
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
@@ -0,0 +1,49 @@
+package ru.javawebinar.topjava.web.json;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+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);
+ }
+ }
+
+ public static String writeAdditionProps(T obj, String addName, Object addValue) {
+ return writeAdditionProps(obj, Map.of(addName, addValue));
+ }
+
+ public static String writeAdditionProps(T obj, Map addProps) {
+ Map map = getMapper().convertValue(obj, new TypeReference<>() {});
+ map.putAll(addProps);
+ return writeValue(map);
+ }
+}
\ 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 000000000..ec601c187
--- /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/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
new file mode 100644
index 000000000..66df6c06e
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
@@ -0,0 +1,68 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.Nullable;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealTo;
+
+import java.net.URI;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = MealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class MealRestController extends AbstractMealController {
+ static final String REST_URL = "/rest/profile/meals";
+
+ @Override
+ @GetMapping("/{id}")
+ public Meal get(@PathVariable int id) {
+ return super.get(id);
+ }
+
+ @Override
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable int id) {
+ super.delete(id);
+ }
+
+ @Override
+ @GetMapping
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void update(@RequestBody Meal meal, @PathVariable int id) {
+ super.update(meal, id);
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@RequestBody Meal meal) {
+ Meal created = super.create(meal);
+
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL + "/{id}")
+ .buildAndExpand(created.getId()).toUri();
+
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @Override
+ @GetMapping("/filter")
+ public List getBetween(
+ @RequestParam @Nullable LocalDate startDate,
+ @RequestParam @Nullable LocalTime startTime,
+ @RequestParam @Nullable LocalDate endDate,
+ @RequestParam @Nullable LocalTime endTime) {
+ return super.getBetween(startDate, startTime, endDate, endTime);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java
new file mode 100644
index 000000000..67715ce7b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java
@@ -0,0 +1,65 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.Nullable;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealTo;
+import ru.javawebinar.topjava.util.ValidationUtil;
+
+import javax.validation.Valid;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/profile/meals", produces = MediaType.APPLICATION_JSON_VALUE)
+public class MealUIController extends AbstractMealController {
+
+ @Override
+ @GetMapping
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @GetMapping( "/{id}")
+ public Meal get(@PathVariable int id) {
+ return super.get(id);
+ }
+
+ @Override
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable int id) {
+ super.delete(id);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult result) {
+ if (result.hasErrors()) {
+ // TODO change to exception handler
+ return ValidationUtil.getErrorResponse(result);
+ }
+ if (meal.isNew()) {
+ super.create(meal);
+ } else {
+ super.update(meal, meal.getId());
+ }
+ return ResponseEntity.ok().build();
+ }
+
+ @Override
+ @GetMapping("/filter")
+ public List getBetween(
+ @RequestParam @Nullable LocalDate startDate,
+ @RequestParam @Nullable LocalTime startTime,
+ @RequestParam @Nullable LocalDate endDate,
+ @RequestParam @Nullable LocalTime endTime) {
+ return super.getBetween(startDate, startTime, endDate, endTime);
+ }
+}
\ 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 000000000..18e3698e8
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -0,0 +1,75 @@
+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 ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.UsersUtil;
+
+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(UserTo userTo) {
+ log.info("create {}", userTo);
+ checkNew(userTo);
+ return service.create(UsersUtil.createNewFromTo(userTo));
+ }
+
+ 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 void update(UserTo userTo, int id) {
+ log.info("update {} with id={}", userTo, id);
+ assureIdConsistent(userTo, id);
+ service.update(userTo);
+ }
+
+ public User getByMail(String email) {
+ log.info("getByEmail {}", email);
+ return service.getByEmail(email);
+ }
+
+ public User getWithMeals(int id) {
+ log.info("getWithMeals {}", id);
+ return service.getWithMeals(id);
+ }
+
+ public void enable(int id, boolean enabled) {
+ log.info(enabled ? "enable {}" : "disable {}", id);
+ service.enable(id, enabled);
+ }
+}
\ 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 000000000..dfc40e6c6
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
@@ -0,0 +1,71 @@
+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);
+ }
+
+ @GetMapping("/{id}/with-meals")
+ public User getWithMeals(@PathVariable int id) {
+ return super.getWithMeals(id);
+ }
+
+ @Override
+ @PatchMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void enable(@PathVariable int id, @RequestParam boolean enabled) {
+ super.enable(id, enabled);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java
new file mode 100644
index 000000000..0f5218560
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.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.validation.BindingResult;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.ValidationUtil;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/admin/users", produces = MediaType.APPLICATION_JSON_VALUE)
+public class AdminUIController extends AbstractUserController {
+
+ @Override
+ @GetMapping
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @GetMapping("/{id}")
+ public User get(@PathVariable int id) {
+ return super.get(id);
+ }
+
+ @Override
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable int id) {
+ super.delete(id);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public ResponseEntity createOrUpdate(@Valid UserTo userTo, BindingResult result) {
+ if (result.hasErrors()) {
+ // TODO change to exception handler
+ return ValidationUtil.getErrorResponse(result);
+ }
+ if (userTo.isNew()) {
+ super.create(userTo);
+ } else {
+ super.update(userTo, userTo.id());
+ }
+ return ResponseEntity.ok().build();
+ }
+
+ @Override
+ @PostMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void enable(@PathVariable int id, @RequestParam boolean enabled) {
+ super.enable(id, enabled);
+ }
+}
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 000000000..c99f2ccaa
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,55 @@
+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 ru.javawebinar.topjava.to.UserTo;
+
+import java.net.URI;
+
+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());
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.CREATED)
+ public ResponseEntity register(@RequestBody UserTo userTo) {
+ User created = super.create(userTo);
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL).build().toUri();
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void update(@RequestBody UserTo userTo) {
+ super.update(userTo, authUserId());
+ }
+
+ @GetMapping("/text")
+ public String testUTF() {
+ return "Русский текст";
+ }
+
+ @GetMapping("/with-meals")
+ public User getWithMeals() {
+ return super.getWithMeals(authUserId());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java
new file mode 100644
index 000000000..657396a7d
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java
@@ -0,0 +1,54 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.support.SessionStatus;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.web.SecurityUtil;
+
+import javax.validation.Valid;
+
+@Controller
+@RequestMapping("/profile")
+public class ProfileUIController extends AbstractUserController {
+
+ @GetMapping
+ public String profile() {
+ return "profile";
+ }
+
+ @PostMapping
+ public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status) {
+ if (result.hasErrors()) {
+ return "profile";
+ } else {
+ super.update(userTo, SecurityUtil.authUserId());
+ SecurityUtil.get().setTo(userTo);
+ status.setComplete();
+ return "redirect:/meals";
+ }
+ }
+
+ @GetMapping("/register")
+ public String register(ModelMap model) {
+ model.addAttribute("userTo", new UserTo());
+ model.addAttribute("register", true);
+ return "profile";
+ }
+
+ @PostMapping("/register")
+ public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) {
+ if (result.hasErrors()) {
+ model.addAttribute("register", true);
+ return "profile";
+ } else {
+ super.create(userTo);
+ status.setComplete();
+ return "redirect:/login?message=app.registered&username=" + userTo.getEmail();
+ }
+ }
+}
\ 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 000000000..05589f71f
--- /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 000000000..a180edc2e
--- /dev/null
+++ b/src/main/resources/db/hsqldb.properties
@@ -0,0 +1,10 @@
+#database.url=jdbc:hsqldb:file:D:/temp/topjava
+database.url=jdbc:hsqldb:mem:topjava
+database.username=sa
+database.password=
+database.driverClassName=org.hsqldb.jdbcDriver
+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 000000000..4bf3d8446
--- /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 000000000..9e0e195e6
--- /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 000000000..a937eb325
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,24 @@
+ DELETE FROM user_role;
+DELETE FROM users;
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+INSERT INTO users (name, email, password, calories_per_day)
+VALUES ('User', 'user@yandex.ru', '{noop}password', 2005),
+ ('Admin', 'admin@gmail.com', '{noop}admin', 1900),
+ ('Guest', 'guest@gmail.com', '{noop}guest', 2000);
+
+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 000000000..f7f5dae4a
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,10 @@
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=postgres
+database.password=1234
+database.driverClassName=org.postgresql.Driver
+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 000000000..2e073681a
--- /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 000000000..e2b565616
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ ${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 000000000..3a75ebb82
--- /dev/null
+++ b/src/main/resources/spring/spring-app.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
\ 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 000000000..73325fee0
--- /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 000000000..80fa16295
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 000000000..971ae4df9
--- /dev/null
+++ b/src/main/resources/spring/spring-mvc.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/plain;charset=UTF-8
+ text/html;charset=UTF-8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml
new file mode 100644
index 000000000..efcda19fe
--- /dev/null
+++ b/src/main/resources/spring/spring-security.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 000000000..9311d5904
--- /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/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception.jsp
new file mode 100644
index 000000000..90f84f4b1
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/exception.jsp
@@ -0,0 +1,26 @@
+<%@ page isErrorPage="true" contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+
+
+
+
+
+
+
+
+
+
${status}
+
+ ${message}
+
+
+
+
+
+
\ No newline at end of file
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 000000000..62f05f9a4
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp
@@ -0,0 +1,30 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
+<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
+
+
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 000000000..cf1331fd5
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp
@@ -0,0 +1,8 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%--https://getbootstrap.com/docs/4.0/examples/sticky-footer/--%>
+
\ 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 000000000..cc302e868
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp
@@ -0,0 +1,29 @@
+<%@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" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%--http://stackoverflow.com/a/24070373/548473--%>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp
new file mode 100644
index 000000000..98bd60fa9
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp
@@ -0,0 +1,14 @@
+<%@ page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/login.jsp b/src/main/webapp/WEB-INF/jsp/login.jsp
new file mode 100644
index 000000000..7f29ad7a7
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/login.jsp
@@ -0,0 +1,75 @@
+<%@ 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="sec" uri="http://www.springframework.org/security/tags" %>
+
+
+
+
+
+
+
+
+
+ ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
+
+
+
+
+
+
+
»
+
+
+
+
+
+
Spring Security,
+
Spring MVC,
+
Spring Data JPA,
+
Spring Security
+ Test,
+
Hibernate ORM,
+
Hibernate Validator,
+
SLF4J,
+
Json Jackson,
+
JSP,
+
JSTL,
+
Apache Tomcat,
+
WebJars,
+
DataTables,
+
EHCACHE,
+
PostgreSQL,
+
HSQLDB,
+
JUnit 5,
+
Hamcrest,
+
AssertJ,
+
jQuery,
+
jQuery plugins,
+
Bootstrap.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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 000000000..f66313bb3
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/meals.jsp
@@ -0,0 +1,117 @@
+<%@ 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" %>
+
+
+
+
+
+
+
+
+
+
+ <%--https://getbootstrap.com/docs/4.0/components/card/--%>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/profile.jsp b/src/main/webapp/WEB-INF/jsp/profile.jsp
new file mode 100644
index 000000000..c6d2df55f
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/profile.jsp
@@ -0,0 +1,43 @@
+<%@ page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="topjava" tagdir="/WEB-INF/tags" %>
+
+
+
+
+
+
+
+
+
+ <%--@elvariable id="userTo" type="ru.javawebinar.topjava.to.UserTo"--%>
+
+
+
${userTo.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 000000000..498bc4943
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/users.jsp
@@ -0,0 +1,84 @@
+<%@ 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" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/tags/inputField.tag b/src/main/webapp/WEB-INF/tags/inputField.tag
new file mode 100644
index 000000000..bc481be2d
--- /dev/null
+++ b/src/main/webapp/WEB-INF/tags/inputField.tag
@@ -0,0 +1,15 @@
+<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+<%@ attribute name="name" required="true" description="Name of corresponding property in bean object" %>
+<%@ attribute name="labelCode" required="true" description="Field label" %>
+<%@ attribute name="inputType" required="false" description="Input type" %>
+
+
+
+
\ 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 000000000..d138fecdb
--- /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 000000000..31dcf2676
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,66 @@
+
+
+ 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
+ /
+
+
+
+
+ springSecurityFilterChain
+ org.springframework.web.filter.DelegatingFilterProxy
+
+
+ springSecurityFilterChain
+ /*
+
+
+
+ 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 000000000..9e8db29aa
--- /dev/null
+++ b/src/main/webapp/resources/css/style.css
@@ -0,0 +1,50 @@
+tr[data-meal-excess="false"] {
+ color: green;
+}
+
+tr[data-meal-excess="true"] {
+ color: red;
+}
+
+.fa {
+ cursor: pointer;
+}
+
+tr[data-user-enabled="false"] {
+ opacity: 0.3;
+}
+
+.error, .message {
+ padding: 10px;
+ border-radius: 4px;
+ font-size: 16px;
+}
+
+.error {
+ color: #a94442;
+ background-color: #f2dede;
+ border: 1px solid #ebccd1;
+}
+
+.message {
+ color: #2f9635;
+ background-color: #c6fbc2;
+ border: 1px solid #9feba6;
+}
+
+/*https://getbootstrap.com/docs/4.0/examples/sticky-footer/sticky-footer.css*/
+html {
+ position: relative;
+ min-height: 100%;
+}
+body {
+ margin-bottom: 60px !important; /* Margin bottom by footer height */
+}
+.footer {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 60px; /* Set the fixed height of the footer here */
+ line-height: 60px; /* Vertically center the text there */
+ background-color: #f5f5f5;
+}
diff --git a/src/main/webapp/resources/images/icon-meal.png b/src/main/webapp/resources/images/icon-meal.png
new file mode 100644
index 000000000..b4fc54ad0
Binary files /dev/null and b/src/main/webapp/resources/images/icon-meal.png differ
diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js
new file mode 100644
index 000000000..e185e99c5
--- /dev/null
+++ b/src/main/webapp/resources/js/topjava.common.js
@@ -0,0 +1,117 @@
+let form;
+
+function makeEditable(datatableOpts) {
+ ctx.datatableApi = $("#datatable").DataTable(
+ {
+ ...datatableOpts, // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Spread_syntax
+ "ajax": {
+ "url": ctx.ajaxUrl,
+ "dataSrc": ""
+ },
+ "paging": false,
+ "info": true
+ }
+ );
+ form = $('#detailsForm');
+
+ $(document).ajaxError(function (event, jqXHR, options, jsExc) {
+ failNoty(jqXHR);
+ });
+
+ // solve problem with cache in IE: https://stackoverflow.com/a/4303862/548473
+ $.ajaxSetup({cache: false});
+
+ var token = $("meta[name='_csrf']").attr("content");
+ var header = $("meta[name='_csrf_header']").attr("content");
+ $(document).ajaxSend(function (e, xhr, options) {
+ xhr.setRequestHeader(header, token);
+ });
+}
+
+function add() {
+ $("#modalTitle").html(i18n["addTitle"]);
+ form.find(":input").val("");
+ $("#editRow").modal();
+}
+
+function updateRow(id) {
+ form.find(":input").val("");
+ $("#modalTitle").html(i18n["editTitle"]);
+ $.get(ctx.ajaxUrl + id, function (data) {
+ $.each(data, function (key, value) {
+ form.find("input[name='" + key + "']").val(value);
+ });
+ $('#editRow').modal();
+ });
+}
+
+function deleteRow(id) {
+ if (confirm(i18n['common.confirm'])) {
+ $.ajax({
+ url: ctx.ajaxUrl + id,
+ type: "DELETE"
+ }).done(function () {
+ ctx.updateTable();
+ successNoty("common.deleted");
+ });
+ }
+}
+
+function updateTableByData(data) {
+ ctx.datatableApi.clear().rows.add(data).draw();
+}
+
+function save() {
+ $.ajax({
+ type: "POST",
+ url: ctx.ajaxUrl,
+ data: form.serialize()
+ }).done(function () {
+ $("#editRow").modal("hide");
+ ctx.updateTable();
+ successNoty("common.saved");
+ });
+}
+
+let failedNote;
+
+function closeNoty() {
+ if (failedNote) {
+ failedNote.close();
+ failedNote = undefined;
+ }
+}
+
+function successNoty(key) {
+ closeNoty();
+ new Noty({
+ text: " " + i18n[key],
+ type: 'success',
+ layout: "bottomRight",
+ timeout: 1000
+ }).show();
+}
+
+function renderEditBtn(data, type, row) {
+ if (type === "display") {
+ return "";
+ }
+}
+
+function renderDeleteBtn(data, type, row) {
+ if (type === "display") {
+ return "";
+ }
+}
+
+function failNoty(jqXHR) {
+ closeNoty();
+ var errorInfo = jqXHR.responseJSON;
+ failedNote = new Noty({
+ text: " " + i18n["common.errorStatus"] + ": " + jqXHR.status +
+ "
" + errorInfo.type + "
" + errorInfo.detail,
+ type: "error",
+ layout: "bottomRight"
+ });
+ failedNote.show()
+}
\ No newline at end of file
diff --git a/src/main/webapp/resources/js/topjava.meals.js b/src/main/webapp/resources/js/topjava.meals.js
new file mode 100644
index 000000000..73ab323b4
--- /dev/null
+++ b/src/main/webapp/resources/js/topjava.meals.js
@@ -0,0 +1,116 @@
+const mealAjaxUrl = "profile/meals/";
+
+// https://stackoverflow.com/a/5064235/548473
+const ctx = {
+ ajaxUrl: mealAjaxUrl,
+ updateTable: function () {
+ $.ajax({
+ type: "GET",
+ url: mealAjaxUrl + "filter",
+ data: $("#filter").serialize()
+ }).done(updateTableByData);
+ }
+};
+
+function clearFilter() {
+ $("#filter")[0].reset();
+ $.get(mealAjaxUrl, updateTableByData);
+}
+
+// http://api.jquery.com/jQuery.ajax/#using-converters
+$.ajaxSetup({
+ converters: {
+ "text json": function (stringData) {
+ return JSON.parse(stringData,
+ function (key, value) {
+ return (key === 'dateTime') ? value.substring(0, 16).replace('T', ' ') : value;
+ }
+ );
+ }
+ }
+});
+
+$(function () {
+ makeEditable({
+ "columns": [
+ {
+ "data": "dateTime"
+ },
+ {
+ "data": "description"
+ },
+ {
+ "data": "calories"
+ },
+ {
+ "render": renderEditBtn,
+ "defaultContent": "",
+ "orderable": false
+ },
+ {
+ "render": renderDeleteBtn,
+ "defaultContent": "",
+ "orderable": false
+ }
+ ],
+ "order": [
+ [
+ 0,
+ "desc"
+ ]
+ ],
+ "createdRow": function (row, data, dataIndex) {
+ $(row).attr("data-meal-excess", data.excess);
+ }
+ });
+
+// http://xdsoft.net/jqplugins/datetimepicker/
+ var startDate = $('#startDate');
+ var endDate = $('#endDate');
+ const dateOptions = {
+ timepicker: false,
+ format: 'Y-m-d',
+ formatDate: 'Y-m-d',
+ };
+ startDate.datetimepicker({
+ ...dateOptions,
+ onShow: function (ct) {
+ this.setOptions({
+ maxDate: endDate.val() ? endDate.val() : false
+ })
+ }
+ });
+ endDate.datetimepicker({
+ ...dateOptions,
+ onShow: function (ct) {
+ this.setOptions({
+ minDate: startDate.val() ? startDate.val() : false
+ })
+ }
+ });
+
+ var startTime = $('#startTime');
+ var endTime = $('#endTime');
+ startTime.datetimepicker({
+ datepicker: false,
+ format: 'H:i',
+ onShow: function (ct) {
+ this.setOptions({
+ maxTime: endTime.val() ? endTime.val() : false
+ })
+ }
+ });
+ endTime.datetimepicker({
+ datepicker: false,
+ format: 'H:i',
+ onShow: function (ct) {
+ this.setOptions({
+ minTime: startTime.val() ? startTime.val() : false
+ })
+ }
+ });
+
+ $('#dateTime').datetimepicker({
+ format: 'Y-m-d H:i'
+ });
+});
\ No newline at end of file
diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js
new file mode 100644
index 000000000..725422291
--- /dev/null
+++ b/src/main/webapp/resources/js/topjava.users.js
@@ -0,0 +1,86 @@
+const userAjaxUrl = "admin/users/";
+
+// https://stackoverflow.com/a/5064235/548473
+const ctx = {
+ ajaxUrl: userAjaxUrl,
+ updateTable: function () {
+ $.get(userAjaxUrl, updateTableByData);
+ }
+}
+
+function enable(chkbox, id) {
+ const enabled = chkbox.is(":checked");
+// https://stackoverflow.com/a/22213543/548473
+ $.ajax({
+ url: userAjaxUrl + id,
+ type: "POST",
+ data: "enabled=" + enabled
+ }).done(function () {
+ chkbox.closest("tr").attr("data-user-enabled", enabled);
+ successNoty(enabled ? "common.enabled" : "common.disabled");
+ }).fail(function () {
+ $(chkbox).prop("checked", !enabled);
+ });
+}
+
+// $(document).ready(function () {
+$(function () {
+ makeEditable({
+ "columns": [
+ {
+ "data": "name"
+ },
+ {
+ "data": "email",
+ "render": function (data, type, row) {
+ if (type === "display") {
+ return "" + data + "";
+ }
+ return data;
+ }
+ },
+ {
+ "data": "roles"
+ },
+ {
+ "data": "enabled",
+ "render": function (data, type, row) {
+ if (type === "display") {
+ return "";
+ }
+ return data;
+ }
+ },
+ {
+ "data": "registered",
+ "render": function (date, type, row) {
+ if (type === "display") {
+ return date.substring(0, 10);
+ }
+ return date;
+ }
+ },
+ {
+ "orderable": false,
+ "defaultContent": "",
+ "render": renderEditBtn
+ },
+ {
+ "orderable": false,
+ "defaultContent": "",
+ "render": renderDeleteBtn
+ }
+ ],
+ "order": [
+ [
+ 0,
+ "asc"
+ ]
+ ],
+ "createdRow": function (row, data, dataIndex) {
+ if (!data.enabled) {
+ $(row).attr("data-user-enabled", false);
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/src/main/webapp/test.html b/src/main/webapp/test.html
new file mode 100644
index 000000000..e50b33277
--- /dev/null
+++ b/src/main/webapp/test.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
\ 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 000000000..43f143cc7
--- /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 000000000..15e01e155
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java
@@ -0,0 +1,83 @@
+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 java.util.function.BiConsumer;
+
+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 usingAssertions(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) {
+ return new Matcher<>(clazz, assertion, iterableAssertion);
+ }
+
+ public static Matcher usingEqualsComparator(Class clazz) {
+ return usingAssertions(clazz,
+ (a, e) -> assertThat(a).isEqualTo(e),
+ (a, e) -> assertThat(a).isEqualTo(e));
+ }
+
+ public static Matcher usingIgnoringFieldsComparator(Class clazz, String... fieldsToIgnore) {
+ return usingAssertions(clazz,
+ (a, e) -> assertThat(a).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(e),
+ (a, e) -> assertThat(a).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(e));
+ }
+
+ public static class Matcher {
+ private final Class clazz;
+ private final BiConsumer assertion;
+ private final BiConsumer, Iterable> iterableAssertion;
+
+ private Matcher(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) {
+ this.clazz = clazz;
+ this.assertion = assertion;
+ this.iterableAssertion = iterableAssertion;
+ }
+
+ public void assertMatch(T actual, T expected) {
+ assertion.accept(actual, expected);
+ }
+
+ @SafeVarargs
+ public final void assertMatch(Iterable actual, T... expected) {
+ assertMatch(actual, List.of(expected));
+ }
+
+ public void assertMatch(Iterable actual, Iterable expected) {
+ iterableAssertion.accept(actual, 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 000000000..6ee8b66dd
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java
@@ -0,0 +1,40 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealTo;
+
+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 MatcherFactory.Matcher TO_MATCHER = MatcherFactory.usingEqualsComparator(MealTo.class);
+
+ 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 000000000..a9e46b208
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java
@@ -0,0 +1,41 @@
+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;
+
+import static ru.javawebinar.topjava.TestUtil.mockAuthorize;
+import static ru.javawebinar.topjava.UserTestData.user;
+
+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", 2000, Role.ADMIN));
+ System.out.println();
+
+ mockAuthorize(user);
+
+ 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/TestUtil.java b/src/test/java/ru/javawebinar/topjava/TestUtil.java
new file mode 100644
index 000000000..3688cd60e
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java
@@ -0,0 +1,23 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+import ru.javawebinar.topjava.model.User;
+
+public class TestUtil {
+
+ public static void mockAuthorize(User user) {
+ SecurityContextHolder.getContext().setAuthentication(
+ new UsernamePasswordAuthenticationToken(new AuthorizedUser(user), null, user.getRoles()));
+ }
+
+ public static RequestPostProcessor userHttpBasic(User user) {
+ return SecurityMockMvcRequestPostProcessors.httpBasic(user.getEmail(), user.getPassword());
+ }
+
+ public static RequestPostProcessor userAuth(User user) {
+ return SecurityMockMvcRequestPostProcessors.authentication(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword()));
+ }
+}
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 000000000..cee6ae92c
--- /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 000000000..e93e9e84b
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java
@@ -0,0 +1,57 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static ru.javawebinar.topjava.MealTestData.*;
+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", "password");
+ public static MatcherFactory.Matcher USER_WITH_MEALS_MATCHER =
+ MatcherFactory.usingAssertions(User.class,
+// No need use ignoringAllOverriddenEquals, see https://assertj.github.io/doc/#breaking-changes
+ (a, e) -> assertThat(a).usingRecursiveComparison().ignoringFields("registered", "meals.user", "password").isEqualTo(e),
+ (a, e) -> {
+ throw new UnsupportedOperationException();
+ });
+
+ 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", 2005, Role.USER);
+ public static final User admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", 1900, Role.ADMIN, Role.USER);
+ public static final User guest = new User(GUEST_ID, "Guest", "guest@gmail.com", "guest", 2000);
+
+ static {
+ user.setMeals(meals);
+ admin.setMeals(List.of(adminMeal2, adminMeal1));
+ }
+
+ 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;
+ }
+
+ public static String jsonWithPassword(User user, String passw) {
+ return JsonUtil.writeAdditionProps(user, "password", passw);
+ }
+}
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 000000000..03770da21
--- /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 000000000..50915ba11
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java
@@ -0,0 +1,82 @@
+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 000000000..f3585dfff
--- /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 000000000..6b00e4481
--- /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 000000000..fb1626c4c
--- /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"), executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+@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 000000000..bf77aaa85
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java
@@ -0,0 +1,97 @@
+
+
+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.*;
+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", 2000, 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", 2000, Role.USER)));
+ validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", 2000, 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())));
+ }
+
+ @Test
+ void enable() {
+ service.enable(USER_ID, false);
+ assertFalse(service.get(USER_ID).isEnabled());
+ service.enable(USER_ID, true);
+ assertTrue(service.get(USER_ID).isEnabled());
+ }
+}
\ 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 000000000..161c93fb5
--- /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 000000000..3638e07e9
--- /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.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.AbstractUserServiceTest;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.Profiles.DATAJPA;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+@ActiveProfiles(DATAJPA)
+class DataJpaUserServiceTest extends AbstractUserServiceTest {
+ @Test
+ void getWithMeals() {
+ User actual = service.getWithMeals(ADMIN_ID);
+ USER_WITH_MEALS_MATCHER.assertMatch(actual, admin);
+ }
+
+ @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 000000000..aef588264
--- /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 000000000..62ca7668c
--- /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 000000000..aaf5dcda9
--- /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 000000000..6d1cd9154
--- /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 000000000..d71aa4adb
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java
@@ -0,0 +1,65 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.jupiter.api.Assumptions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+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;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+
+
+@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 Environment env;
+
+ @Autowired
+ private WebApplicationContext webApplicationContext;
+
+ protected void assumeDataJpa() {
+ Assumptions.assumeTrue(env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.DATAJPA)), "DATA-JPA only");
+ }
+
+ @PostConstruct
+ private void postConstruct() {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(webApplicationContext)
+ .addFilter(CHARACTER_ENCODING_FILTER)
+ .apply(springSecurity())
+ .build();
+ }
+
+ protected ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception {
+ return mockMvc.perform(builder);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java
new file mode 100644
index 000000000..244399662
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java
@@ -0,0 +1,20 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.http.MediaType;
+
+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.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+class ResourceControllerTest extends AbstractControllerTest {
+
+ @Test
+ void resources() throws Exception {
+ perform(get("/resources/css/style.css"))
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.valueOf("text/css")))
+ .andExpect(status().isOk());
+ }
+}
\ 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 000000000..c200fb4e9
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java
@@ -0,0 +1,41 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.jupiter.api.Test;
+
+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.TestUtil.userAuth;
+import static ru.javawebinar.topjava.UserTestData.admin;
+import static ru.javawebinar.topjava.UserTestData.user;
+
+class RootControllerTest extends AbstractControllerTest {
+
+ @Test
+ void getUsers() throws Exception {
+ perform(get("/users")
+ .with(userAuth(admin)))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(view().name("users"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp"));
+ }
+
+ @Test
+ void unAuth() throws Exception {
+ perform(get("/users"))
+ .andDo(print())
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("http://localhost/login"));
+ }
+
+ @Test
+ void getMeals() throws Exception {
+ perform(get("/meals")
+ .with(userAuth(user)))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(view().name("meals"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp"));
+ }
+}
\ 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 000000000..d1a7a5d6c
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java
@@ -0,0 +1,48 @@
+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 ru.javawebinar.topjava.model.User;
+
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static ru.javawebinar.topjava.MealTestData.*;
+import static ru.javawebinar.topjava.UserTestData.jsonWithPassword;
+import static ru.javawebinar.topjava.UserTestData.user;
+
+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);
+ }
+
+ @Test
+ void writeOnlyAccess() {
+ String json = JsonUtil.writeValue(user);
+ System.out.println(json);
+ assertThat(json, not(containsString("password")));
+ String jsonWithPass = jsonWithPassword(user, "newPass");
+ System.out.println(jsonWithPass);
+ User user = JsonUtil.readValue(jsonWithPass, User.class);
+ assertEquals(user.getPassword(), "newPass");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java
new file mode 100644
index 000000000..784bc0479
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java
@@ -0,0 +1,127 @@
+package ru.javawebinar.topjava.web.meal;
+
+
+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.Meal;
+import ru.javawebinar.topjava.service.MealService;
+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.MealTestData.*;
+import static ru.javawebinar.topjava.TestUtil.userHttpBasic;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+import static ru.javawebinar.topjava.UserTestData.user;
+import static ru.javawebinar.topjava.util.MealsUtil.createTo;
+import static ru.javawebinar.topjava.util.MealsUtil.getTos;
+
+class MealRestControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL = MealRestController.REST_URL + '/';
+
+ @Autowired
+ private MealService mealService;
+
+ @Test
+ void get() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + MEAL1_ID)
+ .with(userHttpBasic(user)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MEAL_MATCHER.contentJson(meal1));
+ }
+
+ @Test
+ void getUnauth() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + MEAL1_ID))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void getNotFound() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_MEAL_ID)
+ .with(userHttpBasic(user)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableEntity());
+ }
+
+ @Test
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL + MEAL1_ID)
+ .with(userHttpBasic(user)))
+ .andExpect(status().isNoContent());
+ assertThrows(NotFoundException.class, () -> mealService.get(MEAL1_ID, USER_ID));
+ }
+
+ @Test
+ void deleteNotFound() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL + ADMIN_MEAL_ID)
+ .with(userHttpBasic(user)))
+ .andExpect(status().isUnprocessableEntity());
+ }
+
+ @Test
+ void update() throws Exception {
+ Meal updated = getUpdated();
+ perform(MockMvcRequestBuilders.put(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(user))
+ .content(JsonUtil.writeValue(updated)))
+ .andExpect(status().isNoContent());
+
+ MEAL_MATCHER.assertMatch(mealService.get(MEAL1_ID, USER_ID), updated);
+ }
+
+ @Test
+ void createWithLocation() throws Exception {
+ Meal newMeal = getNew();
+ ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(user))
+ .content(JsonUtil.writeValue(newMeal)))
+ .andExpect(status().isCreated());
+
+ Meal created = MEAL_MATCHER.readFromJson(action);
+ int newId = created.id();
+ newMeal.setId(newId);
+ MEAL_MATCHER.assertMatch(created, newMeal);
+ MEAL_MATCHER.assertMatch(mealService.get(newId, USER_ID), newMeal);
+ }
+
+ @Test
+ void getAll() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL)
+ .with(userHttpBasic(user)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(TO_MATCHER.contentJson(getTos(meals, user.getCaloriesPerDay())));
+ }
+
+ @Test
+ void getBetween() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + "filter")
+ .param("startDate", "2020-01-30").param("startTime", "07:00")
+ .param("endDate", "2020-01-31").param("endTime", "11:00")
+ .with(userHttpBasic(user)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(TO_MATCHER.contentJson(createTo(meal5, true), createTo(meal1, false)));
+ }
+
+ @Test
+ void getBetweenAll() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + "filter?startDate=&endTime=")
+ .with(userHttpBasic(user)))
+ .andExpect(status().isOk())
+ .andExpect(TO_MATCHER.contentJson(getTos(meals, user.getCaloriesPerDay())));
+ }
+}
\ 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 000000000..dc52b0083
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java
@@ -0,0 +1,146 @@
+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.UserTestData;
+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 static org.junit.jupiter.api.Assertions.assertFalse;
+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.TestUtil.userHttpBasic;
+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)
+ .with(userHttpBasic(admin)))
+ .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 getNotFound() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + NOT_FOUND)
+ .with(userHttpBasic(admin)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableEntity());
+ }
+
+ @Test
+ void getByEmail() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL + "by-email?email=" + user.getEmail())
+ .with(userHttpBasic(admin)))
+ .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)
+ .with(userHttpBasic(admin)))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+ assertThrows(NotFoundException.class, () -> userService.get(USER_ID));
+ }
+
+ @Test
+ void deleteNotFound() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL + NOT_FOUND)
+ .with(userHttpBasic(admin)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableEntity());
+ }
+
+ @Test
+ void getUnAuth() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void getForbidden() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL)
+ .with(userHttpBasic(user)))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ void update() throws Exception {
+ User updated = getUpdated();
+ perform(MockMvcRequestBuilders.put(REST_URL + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(admin))
+ .content(jsonWithPassword(updated, updated.getPassword())))
+ .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)
+ .with(userHttpBasic(admin))
+ .content(jsonWithPassword(newUser, newUser.getPassword())))
+ .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)
+ .with(userHttpBasic(admin)))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(admin, guest, user));
+ }
+
+ @Test
+ void getWithMeals() throws Exception {
+ assumeDataJpa();
+ perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID + "/with-meals")
+ .with(userHttpBasic(admin)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_WITH_MEALS_MATCHER.contentJson(admin));
+ }
+
+ @Test
+ void enable() throws Exception {
+ perform(MockMvcRequestBuilders.patch(REST_URL + USER_ID)
+ .param("enabled", "false")
+ .contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(admin)))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+
+ assertFalse(userService.get(USER_ID).isEnabled());
+ }
+}
\ 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 000000000..7568d0f52
--- /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 000000000..c41fa0e6b
--- /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 000000000..9547ccff4
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java
@@ -0,0 +1,89 @@
+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.to.UserTo;
+import ru.javawebinar.topjava.util.UsersUtil;
+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.TestUtil.userHttpBasic;
+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)
+ .with(userHttpBasic(user)))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_MATCHER.contentJson(user));
+ }
+
+ @Test
+ void getUnAuth() throws Exception {
+ perform(MockMvcRequestBuilders.get(REST_URL))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(REST_URL)
+ .with(userHttpBasic(user)))
+ .andExpect(status().isNoContent());
+ USER_MATCHER.assertMatch(userService.getAll(), admin, guest);
+ }
+
+ @Test
+ void register() throws Exception {
+ UserTo newTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500);
+ User newUser = UsersUtil.createNewFromTo(newTo);
+ ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(newTo)))
+ .andDo(print())
+ .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 update() throws Exception {
+ UserTo updatedTo = new UserTo(null, "newName", "user@yandex.ru", "newPassword", 1500);
+ perform(MockMvcRequestBuilders.put(REST_URL).contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(user))
+ .content(JsonUtil.writeValue(updatedTo)))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+
+ USER_MATCHER.assertMatch(userService.get(USER_ID), UsersUtil.updateFromTo(new User(user), updatedTo));
+ }
+
+ @Test
+ void getWithMeals() throws Exception {
+ assumeDataJpa();
+ perform(MockMvcRequestBuilders.get(REST_URL + "/with-meals")
+ .with(userHttpBasic(user)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(USER_WITH_MEALS_MATCHER.contentJson(user));
+ }
+}
\ 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 000000000..803655475
--- /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 000000000..f7e2dbbd4
--- /dev/null
+++ b/src/test/resources/spring/inmemory.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
\ 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 000000000..7c9dfda9a
--- /dev/null
+++ b/src/test/resources/spring/spring-cache.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
\ No newline at end of file