T checkNotFound(T object, String msg) {
+ checkNotFound(object != null, msg);
+ return object;
+ }
+
+ public static void checkNotFound(boolean found, String msg) {
+ if (!found) {
+ throw new NotFoundException("Not found entity with " + msg);
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
new file mode 100644
index 000000000000..1c85842a3497
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
@@ -0,0 +1,15 @@
+package ru.javawebinar.topjava.util.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+/**
+ * User: gkislin
+ * Date: 19.08.2014
+ */
+@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No data found") // 404
+public class NotFoundException extends RuntimeException {
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java
new file mode 100644
index 000000000000..77c4e61c3518
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java
@@ -0,0 +1,83 @@
+package ru.javawebinar.topjava.web;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.HttpStatus;
+import org.springframework.validation.BindException;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.util.exception.ErrorInfo;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Arrays;
+
+/**
+ * User: gkislin
+ * Date: 23.09.2014
+ */
+@ControllerAdvice(annotations = RestController.class)
+public class ExceptionInfoHandler {
+ Logger LOG = LoggerFactory.getLogger(ExceptionInfoHandler.class);
+
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ @ExceptionHandler(NotFoundException.class)
+ @ResponseBody
+ @Order(Ordered.HIGHEST_PRECEDENCE)
+ public ErrorInfo handleError(HttpServletRequest req, NotFoundException e) {
+ return logAndGetErrorInfo(req, e, false);
+ }
+
+ @ResponseStatus(value = HttpStatus.CONFLICT) // 409
+ @ExceptionHandler(DataIntegrityViolationException.class)
+ @ResponseBody
+ @Order(Ordered.HIGHEST_PRECEDENCE + 1)
+ public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) {
+ return logAndGetErrorInfo(req, e, true);
+ }
+
+ @ResponseStatus(value = HttpStatus.BAD_REQUEST) // 400
+ @ExceptionHandler(BindException.class)
+ @ResponseBody
+ @Order(Ordered.HIGHEST_PRECEDENCE + 2)
+ public ErrorInfo bindValidationError(HttpServletRequest req, BindingResult result) {
+ return logAndGetValidationErrorInfo(req, result);
+ }
+
+ @ResponseStatus(value = HttpStatus.BAD_REQUEST) // 400
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseBody
+ @Order(Ordered.HIGHEST_PRECEDENCE + 2)
+ public ErrorInfo restValidationError(HttpServletRequest req, MethodArgumentNotValidException e) {
+ return logAndGetValidationErrorInfo(req, e.getBindingResult());
+ }
+
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ @ExceptionHandler(Exception.class)
+ @ResponseBody
+ @Order(Ordered.LOWEST_PRECEDENCE)
+ public ErrorInfo handleError(HttpServletRequest req, Exception e) {
+ return logAndGetErrorInfo(req, e, true);
+ }
+
+ private ErrorInfo logAndGetValidationErrorInfo(HttpServletRequest req, BindingResult result) {
+ String[] details = result.getFieldErrors().stream()
+ .map(fe -> fe.getField() + ' ' + fe.getDefaultMessage()).toArray(String[]::new);
+
+ LOG.warn("Validation exception at request " + req.getRequestURL() + ": " + Arrays.toString(details));
+ return new ErrorInfo(req.getRequestURL(), "ValidationException", details);
+ }
+
+ private ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException) {
+ if (logException) {
+ LOG.error("Exception at request " + req.getRequestURL(), e);
+ } else {
+ LOG.warn("Exception at request " + req.getRequestURL() + ": " + e.toString());
+ }
+ return new ErrorInfo(req.getRequestURL(), e);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java
new file mode 100644
index 000000000000..eab918d5092b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java
@@ -0,0 +1,36 @@
+package ru.javawebinar.topjava.web;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.servlet.ModelAndView;
+import ru.javawebinar.topjava.AuthorizedUser;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * User: gkislin
+ * Date: 23.09.2014
+ */
+@ControllerAdvice
+public class GlobalControllerExceptionHandler {
+ private static final Logger LOG = LoggerFactory.getLogger(GlobalControllerExceptionHandler.class);
+
+ @ExceptionHandler(Exception.class)
+ @Order(Ordered.LOWEST_PRECEDENCE)
+ ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
+ LOG.error("Exception at request " + req.getRequestURL(), e);
+ ModelAndView mav = new ModelAndView("exception/exception");
+ mav.addObject("exception", e);
+
+ // Interceptor is not invoked, put userTo
+ AuthorizedUser authorizedUser = AuthorizedUser.safeGet();
+ if (authorizedUser != null) {
+ mav.addObject("userTo", authorizedUser.getUserTo());
+ }
+ return mav;
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java
new file mode 100644
index 000000000000..9a2a04172fb1
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java
@@ -0,0 +1,95 @@
+package ru.javawebinar.topjava.web;
+
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.support.SessionStatus;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.UserUtil;
+import ru.javawebinar.topjava.web.user.AbstractUserController;
+
+import javax.validation.Valid;
+
+/**
+ * User: gkislin
+ * Date: 22.08.2014
+ */
+@Controller
+public class RootController extends AbstractUserController {
+
+ @RequestMapping(value = "/", method = RequestMethod.GET)
+ public String root() {
+ return "redirect:meals";
+ }
+
+ // @Secured("ROLE_ADMIN")
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ @RequestMapping(value = "/users", method = RequestMethod.GET)
+ public String userList() {
+ return "userList";
+ }
+
+ @RequestMapping(value = "/meals", method = RequestMethod.GET)
+ public String mealList() {
+ return "mealList";
+ }
+
+ @RequestMapping(value = "/login", method = RequestMethod.GET)
+ public String login(ModelMap model,
+ @RequestParam(value = "error", required = false) boolean error,
+ @RequestParam(value = "message", required = false) String message) {
+
+ model.put("error", error);
+ model.put("message", message);
+ return "login";
+ }
+
+ @RequestMapping(value = "/profile", method = RequestMethod.GET)
+ public String profile() {
+ return "profile";
+ }
+
+ @RequestMapping(value = "/profile", method = RequestMethod.POST)
+ public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status) {
+ if (!result.hasErrors()) {
+ try {
+ userTo.setId(AuthorizedUser.id());
+ super.update(userTo);
+ AuthorizedUser.get().update(userTo);
+ status.setComplete();
+ return "redirect:meals";
+ } catch (DataIntegrityViolationException ex) {
+ result.rejectValue("email", "exception.duplicate_email");
+ }
+ }
+ return "profile";
+ }
+
+ @RequestMapping(value = "/register", method = RequestMethod.GET)
+ public String register(ModelMap model) {
+ model.addAttribute("userTo", new UserTo());
+ model.addAttribute("register", true);
+ return "profile";
+ }
+
+ @RequestMapping(value = "/register", method = RequestMethod.POST)
+ public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) {
+ if (!result.hasErrors()) {
+ try {
+ super.create(UserUtil.createNewFromTo(userTo));
+ status.setComplete();
+ return "redirect:login?message=app.registered";
+ } catch (DataIntegrityViolationException ex) {
+ result.rejectValue("email", "exception.duplicate_email");
+ }
+ }
+ model.addAttribute("register", true);
+ return "profile";
+ }
+}
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 000000000000..d667454749b7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java
@@ -0,0 +1,24 @@
+package ru.javawebinar.topjava.web.interceptor;
+
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+import ru.javawebinar.topjava.AuthorizedUser;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This interceptor adds the user to the model of every requests managed
+ */
+public class ModelInterceptor extends HandlerInterceptorAdapter {
+
+ @Override
+ public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+ if (modelAndView != null && !modelAndView.isEmpty()) {
+ AuthorizedUser authorizedUser = AuthorizedUser.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 000000000000..31d55d51bb51
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java
@@ -0,0 +1,39 @@
+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;
+
+/**
+ * User: gkislin
+ * Date: 26.05.2014
+ *
+ * Handling Hibernate lazy-loading
+ *
+ * @link https://github.com/FasterXML/jackson
+ * @link https://github.com/FasterXML/jackson-datatype-hibernate
+ * @link http://wiki.fasterxml.com/JacksonHowToCustomSerializers
+ */
+public class JacksonObjectMapper extends ObjectMapper {
+
+ private static final ObjectMapper MAPPER = new JacksonObjectMapper();
+
+ private JacksonObjectMapper() {
+ registerModule(new Hibernate5Module());
+
+ registerModule(new JavaTimeModule());
+ configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+
+ setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
+ setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ }
+
+ public static ObjectMapper getMapper() {
+ return MAPPER;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
new file mode 100644
index 000000000000..c15e266c4dfe
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
@@ -0,0 +1,41 @@
+package ru.javawebinar.topjava.web.json;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+import java.io.IOException;
+import java.util.List;
+
+import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper;
+
+/**
+ * User: gkislin
+ * Date: 30.04.2014
+ */
+public class JsonUtil {
+
+ public static List readValues(String json, Class clazz) {
+ ObjectReader reader = getMapper().readerFor(clazz);
+ try {
+ return reader.readValues(json).readAll();
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Invalid read array from JSON:\n'" + json + "'", e);
+ }
+ }
+
+ public static T readValue(String json, Class clazz) {
+ try {
+ return getMapper().readValue(json, clazz);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Invalid read from JSON:\n'" + json + "'", e);
+ }
+ }
+
+ public static String writeValue(T obj) {
+ try {
+ return getMapper().writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e);
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractUserMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractUserMealController.java
new file mode 100644
index 000000000000..b8d75b362992
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractUserMealController.java
@@ -0,0 +1,68 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.model.UserMeal;
+import ru.javawebinar.topjava.service.UserMealService;
+import ru.javawebinar.topjava.to.UserMealWithExceed;
+import ru.javawebinar.topjava.util.TimeUtil;
+import ru.javawebinar.topjava.util.UserMealsUtil;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+public abstract class AbstractUserMealController {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractUserMealController.class);
+
+ @Autowired
+ private UserMealService service;
+
+ public UserMeal get(int id) {
+ int userId = AuthorizedUser.id();
+ LOG.info("get meal {} for User {}", id, userId);
+ return service.get(id, userId);
+ }
+
+ public void delete(int id) {
+ int userId = AuthorizedUser.id();
+ LOG.info("delete meal {} for User {}", id, userId);
+ service.delete(id, userId);
+ }
+
+ public List getAll() {
+ int userId = AuthorizedUser.id();
+ LOG.info("getAll for User {}", userId);
+ return UserMealsUtil.getWithExceeded(service.getAll(userId), AuthorizedUser.getCaloriesPerDay());
+ }
+
+ public void update(UserMeal meal, int id) {
+ meal.setId(id);
+ int userId = AuthorizedUser.id();
+ LOG.info("update {} for User {}", meal, userId);
+ service.update(meal, userId);
+ }
+
+ public UserMeal create(UserMeal meal) {
+ meal.setId(null);
+ int userId = AuthorizedUser.id();
+ LOG.info("create {} for User {}", meal, userId);
+ return service.save(meal, userId);
+ }
+
+ public List getBetween(LocalDate startDate, LocalTime startTime, LocalDate endDate, LocalTime endTime) {
+ int userId = AuthorizedUser.id();
+ LOG.info("getBetween dates {} - {} for time {} - {} for User {}", startDate, endDate, startTime, endTime, userId);
+ return UserMealsUtil.getFilteredWithExceeded(
+ service.getBetweenDates(
+ startDate != null ? startDate : TimeUtil.MIN_DATE, endDate != null ? endDate : TimeUtil.MAX_DATE, userId
+ ), startTime != null ? startTime : LocalTime.MIN, endTime != null ? endTime : LocalTime.MAX, AuthorizedUser.getCaloriesPerDay()
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/UserMealAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/meal/UserMealAjaxController.java
new file mode 100644
index 000000000000..60e55c4e5b32
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/UserMealAjaxController.java
@@ -0,0 +1,61 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.UserMeal;
+import ru.javawebinar.topjava.to.UserMealWithExceed;
+
+import javax.validation.Valid;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+@RestController
+@RequestMapping(value = "/ajax/profile/meals")
+public class UserMealAjaxController extends AbstractUserMealController {
+
+/*
+ Other validation solution:
+ http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-initbinder
+
+ @InitBinder
+ protected void initBinder(WebDataBinder binder) {
+ binder.setRequiredFields("dateTime", "description", "calories");
+ }
+*/
+
+ @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @RequestMapping(value = "/{id}")
+ public UserMeal get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @RequestMapping(method = RequestMethod.POST)
+ public void updateOrCreate(@Valid UserMeal meal) {
+ if (meal.isNew()) {
+ super.create(meal);
+ } else {
+ super.update(meal, meal.getId());
+ }
+ }
+
+ @RequestMapping(value = "/filter", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
+ public List getBetween(
+ @RequestParam(value = "startDate", required = false) LocalDate startDate, @RequestParam(value = "startTime", required = false) LocalTime startTime,
+ @RequestParam(value = "endDate", required = false) LocalDate endDate, @RequestParam(value = "endTime", required = false) 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/UserMealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/UserMealRestController.java
new file mode 100644
index 000000000000..e7beb0e3e7a4
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/UserMealRestController.java
@@ -0,0 +1,71 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.springframework.format.annotation.DateTimeFormat;
+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.UserMeal;
+import ru.javawebinar.topjava.to.UserMealWithExceed;
+
+import javax.validation.Valid;
+import java.net.URI;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+@RestController
+@RequestMapping(value = UserMealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class UserMealRestController extends AbstractUserMealController {
+ static final String REST_URL = "/rest/profile/meals";
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.GET)
+ public UserMeal get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @RequestMapping(method = RequestMethod.GET)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
+ public void update(@Valid @RequestBody UserMeal meal, @PathVariable("id") int id) {
+ super.update(meal, id);
+ }
+
+ @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@Valid @RequestBody UserMeal meal) {
+ UserMeal created = super.create(meal);
+
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL + "/{id}")
+ .buildAndExpand(created.getId()).toUri();
+
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @RequestMapping(value = "/between", method = RequestMethod.GET)
+ public List getBetween(
+ @RequestParam(value = "startDateTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDateTime,
+ @RequestParam(value = "endDateTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDateTime) {
+ return super.getBetween(startDateTime.toLocalDate(), startDateTime.toLocalTime(), endDateTime.toLocalDate(), endDateTime.toLocalTime());
+ }
+
+ @RequestMapping(value = "/filter", method = RequestMethod.GET)
+ public List getBetween(
+ @RequestParam(value = "startDate", required = false) LocalDate startDate, @RequestParam(value = "startTime", required = false) LocalTime startTime,
+ @RequestParam(value = "endDate", required = false) LocalDate endDate, @RequestParam(value = "endTime", required = false) LocalTime endTime) {
+ return super.getBetween(startDate, startTime, endDate, endTime);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
new file mode 100644
index 000000000000..003c99cca6ba
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -0,0 +1,83 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+import ru.javawebinar.topjava.Profiles;
+import ru.javawebinar.topjava.model.BaseEntity;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+import ru.javawebinar.topjava.to.UserTo;
+
+import javax.validation.ValidationException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * User: gkislin
+ */
+public abstract class AbstractUserController {
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ @Autowired
+ private UserService service;
+
+ private boolean systemUserForbiddenModification;
+
+ @Autowired
+ public void setEnvironment(Environment environment) {
+ systemUserForbiddenModification = Arrays.asList(environment.getActiveProfiles()).contains(Profiles.HEROKU);
+ }
+
+ public void checkModificationAllowed(Integer id) {
+ if (systemUserForbiddenModification && id < BaseEntity.START_SEQ + 2) {
+ throw new ValidationException("Admin/User modification is not allowed. Register » your own please.");
+ }
+ }
+
+ public List getAll() {
+ log.info("getAll");
+ return service.getAll();
+ }
+
+ public User get(int id) {
+ log.info("get " + id);
+ return service.get(id);
+ }
+
+ public User create(User user) {
+ user.setId(null);
+ log.info("create " + user);
+ return service.save(user);
+ }
+
+ public void delete(int id) {
+ checkModificationAllowed(id);
+ log.info("delete " + id);
+ service.delete(id);
+ }
+
+ public void update(User user, int id) {
+ checkModificationAllowed(id);
+ user.setId(id);
+ log.info("update " + user);
+ service.update(user);
+ }
+
+ public void update(UserTo userTo) {
+ checkModificationAllowed(userTo.getId());
+ log.info("update " + userTo);
+ service.update(userTo);
+ }
+
+ public User getByMail(String email) {
+ log.info("getByEmail " + email);
+ return service.getByEmail(email);
+ }
+
+ public void enable(int id, boolean enabled) {
+ log.info((enabled ? "enable " : "disable ") + id);
+ service.enable(id, enabled);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java
new file mode 100644
index 000000000000..37df3aa44339
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java
@@ -0,0 +1,62 @@
+package ru.javawebinar.topjava.web.user;
+
+import com.fasterxml.jackson.annotation.JsonView;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.View;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.UserUtil;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * User: grigory.kislin
+ */
+@RestController
+@RequestMapping("/ajax/admin/users")
+public class AdminAjaxController extends AbstractUserController {
+
+ @Autowired
+ private MessageSource messageSource;
+
+ @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @JsonView(View.UI.class)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ @JsonView(View.UI.class)
+ public User get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @RequestMapping(method = RequestMethod.POST)
+ public void createOrUpdate(@Valid UserTo userTo) {
+ try {
+ if (userTo.isNew()) {
+ super.create(UserUtil.createNewFromTo(userTo));
+ } else {
+ super.update(userTo);
+ }
+ } catch (DataIntegrityViolationException e) {
+ throw new DataIntegrityViolationException(messageSource.getMessage("exception.duplicate_email", null, LocaleContextHolder.getLocale()));
+ }
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.POST)
+ public void enabled(@PathVariable("id") int id, @RequestParam("enabled") boolean enabled) {
+ super.enable(id, enabled);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
new file mode 100644
index 000000000000..03d1ef4d2914
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
@@ -0,0 +1,60 @@
+package ru.javawebinar.topjava.web.user;
+
+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 javax.validation.Valid;
+import java.net.URI;
+import java.util.List;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+@RestController
+@RequestMapping(AdminRestController.REST_URL)
+public class AdminRestController extends AbstractUserController {
+ static final String REST_URL = "/rest/admin/users";
+
+ @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ public User get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+ @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@Valid @RequestBody User user) {
+ User created = super.create(user);
+
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL + "/{id}")
+ .buildAndExpand(created.getId()).toUri();
+
+// HttpHeaders httpHeaders = new HttpHeaders();
+// httpHeaders.setLocation(uriOfNewResource);
+
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
+ public void update(@Valid @RequestBody User user, @PathVariable("id") int id) {
+ super.update(user, id);
+ }
+
+ @RequestMapping(value = "/by", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ public User getByMail(@RequestParam("email") String email) {
+ return super.getByMail(email);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
new file mode 100644
index 000000000000..cd42614f7264
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,43 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+
+import javax.validation.Valid;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+@RestController
+@RequestMapping(ProfileRestController.REST_URL)
+public class ProfileRestController extends AbstractUserController {
+ static final String REST_URL = "/rest/profile";
+
+ @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
+ public User get() {
+ return super.get(AuthorizedUser.id());
+ }
+
+ @RequestMapping(method = RequestMethod.DELETE)
+ public void delete() {
+ super.delete(AuthorizedUser.id());
+ }
+
+ @RequestMapping(method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
+ public void update(@Valid @RequestBody UserTo userTo) {
+ userTo.setId(AuthorizedUser.id());
+ super.update(userTo);
+ }
+
+ @RequestMapping(value = "/text", method = RequestMethod.GET)
+ public String testUTF() {
+ return "Русский текст";
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml
new file mode 100644
index 000000000000..b0275e589a4a
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/cache/ehcache.xsd b/src/main/resources/cache/ehcache.xsd
new file mode 100644
index 000000000000..bfc19ddb1e7d
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xsd
@@ -0,0 +1,419 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/db/heroku.properties b/src/main/resources/db/heroku.properties
new file mode 100644
index 000000000000..c8146ba6f139
--- /dev/null
+++ b/src/main/resources/db/heroku.properties
@@ -0,0 +1,5 @@
+jpa.showSql=false
+hibernate.format_sql=false
+hibernate.use_sql_comments=false
+database.init=false
+jdbc.initLocation=initDB.sql
\ No newline at end of file
diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties
new file mode 100644
index 000000000000..7d4d3593e48c
--- /dev/null
+++ b/src/main/resources/db/hsqldb.properties
@@ -0,0 +1,9 @@
+#database.url=jdbc:hsqldb:file:D:/temp/topjava
+database.url=jdbc:hsqldb:mem:topjava
+database.username=sa
+database.password=
+database.init=true
+jdbc.initLocation=initDB_hsql.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql
new file mode 100644
index 000000000000..8eaf64a335f5
--- /dev/null
+++ b/src/main/resources/db/initDB.sql
@@ -0,0 +1,36 @@
+DROP TABLE IF EXISTS user_roles;
+DROP TABLE IF EXISTS meals;
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS global_seq;
+
+CREATE SEQUENCE global_seq START 100000;
+
+CREATE TABLE users
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ name VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ password VARCHAR NOT NULL,
+ registered TIMESTAMP DEFAULT now(),
+ enabled BOOL DEFAULT TRUE,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
+
+CREATE TABLE user_roles
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meals (
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ user_id INTEGER NOT NULL,
+ date_time TIMESTAMP NOT NULL,
+ description TEXT NOT NULL,
+ calories INT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals (user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql
new file mode 100644
index 000000000000..c2e84c4e0096
--- /dev/null
+++ b/src/main/resources/db/initDB_hsql.sql
@@ -0,0 +1,45 @@
+DROP TABLE user_roles
+IF EXISTS;
+DROP TABLE meals
+IF EXISTS;
+DROP TABLE users
+IF EXISTS;
+DROP SEQUENCE global_seq
+IF EXISTS;
+
+CREATE SEQUENCE GLOBAL_SEQ
+ AS INTEGER
+ START WITH 100000;
+
+CREATE TABLE users
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ name VARCHAR(255),
+ email VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ registered TIMESTAMP DEFAULT now(),
+ enabled BOOLEAN DEFAULT TRUE,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON USERS (email);
+
+CREATE TABLE user_roles
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR(255),
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES USERS (id)
+ ON DELETE CASCADE
+);
+
+CREATE TABLE meals
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ date_time TIMESTAMP NOT NULL,
+ description VARCHAR(255) NOT NULL,
+ calories INT NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES USERS (id)
+ ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals (user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql
new file mode 100644
index 000000000000..6629ce2ae620
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,27 @@
+DELETE FROM user_roles;
+DELETE FROM meals;
+DELETE FROM users;
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+-- password
+INSERT INTO users (name, email, password, calories_per_day)
+VALUES ('User', 'user@yandex.ru', '$2a$10$Sh0ZD2NFrzRRJJEKEWn8l.92ROEuzlVyzB9SV1AM8fdluPR0aC1ni', 2005);
+
+-- admin
+INSERT INTO users (name, email, password, calories_per_day)
+VALUES ('Admin', 'admin@gmail.com', '$2a$10$WejOLxVuXRpOgr4IlzQJ.eT4UcukNqHlAiOVZj1P/nmc8WbpMkiju', 1900);
+
+INSERT INTO user_roles (role, user_id) VALUES
+ ('ROLE_USER', 100000),
+ ('ROLE_ADMIN', 100001),
+ ('ROLE_USER', 100001);
+
+INSERT INTO meals (date_time, description, calories, user_id) VALUES
+ ('2015-05-30 10:00:00', 'Завтрак', 500, 100000),
+ ('2015-05-30 13:00:00', 'Обед', 1000, 100000),
+ ('2015-05-30 20:00:00', 'Ужин', 500, 100000),
+ ('2015-05-31 10:00:00', 'Завтрак', 500, 100000),
+ ('2015-05-31 13:00:00', 'Обед', 1000, 100000),
+ ('2015-05-31 20:00:00', 'Ужин', 510, 100000),
+ ('2015-06-01 14:00:00', 'Админ ланч', 510, 100001),
+ ('2015-06-01 21:00:00', 'Админ ужин', 1500, 100001);
diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties
new file mode 100644
index 000000000000..d4ebb0565acc
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,11 @@
+#database.url=jdbc:postgresql://ec2-54-217-202-110.eu-west-1.compute.amazonaws.com:5432/dehm6lvm8bink0?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory
+#database.username=wegxlfzjjgxaxy
+#database.password=SSQyKKE_e93kiUCR-ehzMcKCxZ
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=user
+database.password=password
+database.init=true
+jdbc.initLocation=initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties
new file mode 100644
index 000000000000..2e073681ad16
--- /dev/null
+++ b/src/main/resources/db/tomcat.properties
@@ -0,0 +1,5 @@
+database.init=false
+jdbc.initLocation=initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 000000000000..21a531c142dd
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ true
+
+
+
+
+
+
+ ${TOPJAVA_ROOT}/log/topjava.log
+
+
+ UTF-8
+ %date %-5level %logger{0} [%file:%line] %msg%n
+
+
+
+
+
+ UTF-8
+ %-5level %logger{0} [%file:%line] %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml
new file mode 100644
index 000000000000..1fcc7b263e99
--- /dev/null
+++ b/src/main/resources/spring/spring-app.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml
new file mode 100644
index 000000000000..818ca8090aca
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml
new file mode 100644
index 000000000000..ed752cbd3022
--- /dev/null
+++ b/src/main/resources/spring/spring-mvc.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 000000000000..2b70fbf65073
--- /dev/null
+++ b/src/main/resources/spring/spring-security.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-tools.xml b/src/main/resources/spring/spring-tools.xml
new file mode 100644
index 000000000000..41ca4b76f124
--- /dev/null
+++ b/src/main/resources/spring/spring-tools.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/tomcat/context.xml b/src/main/resources/tomcat/context.xml
new file mode 100644
index 000000000000..fd26e2ff8575
--- /dev/null
+++ b/src/main/resources/tomcat/context.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+ WEB-INF/web.xml
+ ${catalina.base}/conf/web.xml
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/exception/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp
new file mode 100644
index 000000000000..54fbf1643205
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp
@@ -0,0 +1,24 @@
+<%@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" %>
+
+
+
+
+
+
+
+
+
+
Application error:
+ ${exception.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 000000000000..63ff63bba896
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp
@@ -0,0 +1,28 @@
+<%@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" %>
+<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
+<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp
new file mode 100644
index 000000000000..5a27552e3250
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp
@@ -0,0 +1,10 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp
new file mode 100644
index 000000000000..6df2179e85bd
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp
@@ -0,0 +1,18 @@
+<%@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" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+
+
+
+
+
+
+
+
+ ${pageContext.request.requestURL}
+
+
+
+
+
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 000000000000..47a581c1219f
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp
@@ -0,0 +1,8 @@
+<%@page contentType="text/javascript" pageEncoding="UTF-8"%>
+ <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+ <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%>
+ var i18n = [];
+
+ i18n['${key}'] = ' ';
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/lang.jsp b/src/main/webapp/WEB-INF/jsp/fragments/lang.jsp
new file mode 100644
index 000000000000..3583cdfa7d8c
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/lang.jsp
@@ -0,0 +1,14 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+
+
+ ${pageContext.response.locale}
+
+
+
\ 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 000000000000..e82b210d7677
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/login.jsp
@@ -0,0 +1,88 @@
+<%@ 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="form" uri="http://www.springframework.org/tags/form" %>
+
+
+
+
+
+
+
+
+
+
+ ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
+
+
+
+
+
+
+
+
+
+
User login: user@yandex.ru / password
+
+
Admin login: admin@gmail.com / admin
+
+
»
+
+
Стек технологий: Spring Security ,
+ Spring MVC ,
+ Spring Data JPA ,
+ Spring Security
+ Test ,
+ Hibernate ORM ,
+ Hibernate Validator ,
+ SLF4J ,
+ Json Jackson ,
+ JSP ,
+ JSTL ,
+ Apache Tomcat ,
+ WebJars ,
+ DataTables plugin ,
+ Ehcache ,
+ PostgreSQL ,
+ JUnit ,
+ Hamcrest ,
+ jQuery ,
+ jQuery notification ,
+ Bootstrap .
+
+
+
+
+
Java Enterprise проект с
+ регистрацией/авторизацией и интерфейсом на основе ролей (USER, ADMIN).
+ Администратор может создавать/редактировать/удалять/пользователей, а пользователь - управлять своим
+ профилем и данными (день, еда, калории) через UI (по AJAX) и по REST интерфейсу с базовой авторизацией.
+ Возможна фильтрация данных по датам и времени, при этом цвет записи таблицы еды зависит от того, превышает ли
+ сумма
+ калорий за день норму (редактируемый параметр в профиле пользователя).
+ Весь REST интерфейс покрывается JUnit тестами, используя Spring MVC Test и Spring Security Test.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/mealList.jsp b/src/main/webapp/WEB-INF/jsp/mealList.jsp
new file mode 100644
index 000000000000..a95e3919eb94
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/mealList.jsp
@@ -0,0 +1,125 @@
+<%@ 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="form" uri="http://www.springframework.org/tags/form" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Date
+ Description
+ Calories
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 000000000000..e2cc0affaec4
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/profile.jsp
@@ -0,0 +1,42 @@
+
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ 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" %>
+
+
+
+
+
+
+
+
+
+
+
${register ? 'Register new' : userTo.name.concat(' profile')}
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/userList.jsp b/src/main/webapp/WEB-INF/jsp/userList.jsp
new file mode 100644
index 000000000000..e9fc7542c3a7
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/userList.jsp
@@ -0,0 +1,108 @@
+<%@ 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="form" uri="http://www.springframework.org/tags/form" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 000000000000..6f4f5ff5adc2
--- /dev/null
+++ b/src/main/webapp/WEB-INF/tags/inputField.tag
@@ -0,0 +1,23 @@
+<%@ 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="label" required="true" description="Field label" %>
+<%@ attribute name="inputType" required="false" description="Input type" %>
+
+
+
+
+
${label}
+
+
+
+
+
+
+
+ ${status.errorMessage}
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld
new file mode 100644
index 000000000000..b9ca44589af4
--- /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.TimeUtil
+ java.lang.String toString(java.time.LocalDateTime)
+
+
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000000..ee368c28ab9c
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,87 @@
+
+
+ Topjava
+
+
+ spring.profiles.default
+ postgres,datajpa
+
+
+
+ contextConfigLocation
+
+ classpath:spring/spring-app.xml
+ classpath:spring/spring-db.xml
+
+
+
+
+
+ org.springframework.web.context.ContextLoaderListener
+
+
+ mvc-dispatcher
+ org.springframework.web.servlet.DispatcherServlet
+
+ contextConfigLocation
+ classpath:spring/spring-mvc.xml
+
+ 1
+
+
+ mvc-dispatcher
+ /
+
+
+
+
+
+ encodingFilter
+ org.springframework.web.filter.CharacterEncodingFilter
+
+ encoding
+ UTF-8
+
+
+ forceEncoding
+ true
+
+
+
+ encodingFilter
+ /*
+
+
+
+
+ springSecurityFilterChain
+ org.springframework.web.filter.DelegatingFilterProxy
+
+
+ springSecurityFilterChain
+ /*
+
+
diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css
new file mode 100644
index 000000000000..703d68c2d465
--- /dev/null
+++ b/src/main/webapp/resources/css/style.css
@@ -0,0 +1,30 @@
+.normal {
+ color: green;
+}
+
+.exceeded {
+ color: red;
+}
+
+.footer {
+ padding-bottom: 15px;
+}
+
+.error, .message {
+ padding: 10px;
+ margin-bottom: 20px;
+ 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;
+}
\ No newline at end of file
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 000000000000..b4fc54ad0129
Binary files /dev/null and b/src/main/webapp/resources/images/icon-meal.png differ
diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js
new file mode 100644
index 000000000000..a941eff9793c
--- /dev/null
+++ b/src/main/webapp/resources/js/datatablesUtil.js
@@ -0,0 +1,120 @@
+var form;
+
+function makeEditable() {
+ form = $('#detailsForm');
+
+ form.submit(function () {
+ save();
+ return false;
+ });
+
+ $(document).ajaxError(function (event, jqXHR, options, jsExc) {
+ failNoty(event, jqXHR, options, jsExc);
+ });
+
+ 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(key) {
+ form.find(":input").val("");
+ $('#modalTitle').html(i18n[key]);
+ $('#editRow').modal();
+}
+
+function updateRow(id, key) {
+ $('#modalTitle').html(i18n[key]);
+ $.get(ajaxUrl + id, function (data) {
+ $.each(data, function (key, value) {
+ form.find("input[name='" + key + "']").val(
+ key === "dateTime" ? value.replace('T', ' ').substr(0, 16) : value
+ );
+ });
+ $('#editRow').modal();
+ });
+}
+
+function deleteRow(id) {
+ $.ajax({
+ url: ajaxUrl + id,
+ type: 'DELETE',
+ success: function () {
+ updateTable();
+ successNoty('Deleted');
+ }
+ });
+}
+
+function enable(chkbox, id) {
+ var enabled = chkbox.is(":checked");
+ chkbox.closest('tr').css("text-decoration", enabled ? "none" : "line-through");
+ $.ajax({
+ url: ajaxUrl + id,
+ type: 'POST',
+ data: 'enabled=' + enabled,
+ success: function () {
+ successNoty(enabled ? 'Enabled' : 'Disabled');
+ }
+ });
+}
+
+function updateTableByData(data) {
+ datatableApi.clear().rows.add(data).draw();
+}
+
+function save() {
+ $.ajax({
+ type: "POST",
+ url: ajaxUrl,
+ data: form.serialize(),
+ success: function () {
+ $('#editRow').modal('hide');
+ updateTable();
+ successNoty('Saved');
+ }
+ });
+}
+
+var failedNote;
+
+function closeNoty() {
+ if (failedNote) {
+ failedNote.close();
+ failedNote = undefined;
+ }
+}
+
+function successNoty(text) {
+ closeNoty();
+ noty({
+ text: text,
+ type: 'success',
+ layout: 'bottomRight',
+ timeout: true
+ });
+}
+
+function failNoty(event, jqXHR, options, jsExc) {
+ closeNoty();
+ var errorInfo = $.parseJSON(jqXHR.responseText);
+ failedNote = noty({
+ text: 'Failed: ' + jqXHR.statusText + '
' + errorInfo.cause + '
' + errorInfo.details.join('
'),
+ type: 'error',
+ layout: 'bottomRight'
+ });
+}
+
+function renderEditBtn(type, row, key) {
+ if (type == 'display') {
+ return '
' + i18n['common.edit'] + ' ';
+ }
+}
+
+function renderDeleteBtn(data, type, row) {
+ if (type == 'display') {
+ return '
' + i18n['common.delete'] + ' ';
+ }
+}
\ No newline at end of file
diff --git a/src/main/webapp/resources/js/mealDatatables.js b/src/main/webapp/resources/js/mealDatatables.js
new file mode 100644
index 000000000000..232d88439dfd
--- /dev/null
+++ b/src/main/webapp/resources/js/mealDatatables.js
@@ -0,0 +1,107 @@
+var ajaxUrl = 'ajax/profile/meals/';
+var datatableApi;
+
+function updateTable() {
+ $.ajax({
+ type: "POST",
+ url: ajaxUrl + 'filter',
+ data: $('#filter').serialize(),
+ success: updateTableByData
+ });
+ return false;
+}
+
+$(function () {
+ datatableApi = $('#datatable').DataTable({
+ "ajax": {
+ "url": ajaxUrl,
+ "dataSrc": ""
+ },
+ "paging": false,
+ "info": true,
+ "columns": [
+ {
+ "data": "dateTime",
+ "render": function (date, type, row) {
+ if (type == 'display') {
+ return date.replace('T', ' ').substr(0, 16);
+ }
+ return date;
+ }
+ },
+ {
+ "data": "description"
+ },
+ {
+ "data": "calories"
+ },
+ {
+ "defaultContent": "",
+ "orderable": false,
+ "render": function (date, type, row) {
+ return renderEditBtn(type, row, 'meals.edit');
+ }
+ },
+ {
+ "defaultContent": "",
+ "orderable": false,
+ "render": renderDeleteBtn
+
+ }
+ ],
+ "order": [
+ [
+ 0,
+ "desc"
+ ]
+ ],
+ "createdRow": function (row, data, dataIndex) {
+ $(row).addClass(data.exceed ? 'exceeded' : 'normal');
+ },
+ "initComplete": function () {
+ $('#filter').submit(function () {
+ updateTable();
+ return false;
+ });
+
+ var startDate = $('#startDate');
+ var endDate = $('#endDate');
+
+ startDate.datetimepicker({
+ timepicker: false,
+ format: 'Y-m-d',
+ lang: 'ru',
+ formatDate: 'Y-m-d',
+ onShow: function (ct) {
+ this.setOptions({
+ maxDate: endDate.val() ? endDate.val() : false
+ })
+ }
+ });
+ endDate.datetimepicker({
+ timepicker: false,
+ format: 'Y-m-d',
+ lang: 'ru',
+ formatDate: 'Y-m-d',
+ onShow: function (ct) {
+ this.setOptions({
+ minDate: startDate.val() ? startDate.val() : false
+ })
+ }
+ });
+
+ $('.time-picker').datetimepicker({
+ datepicker: false,
+ format: 'H:i',
+ lang: 'ru'
+ });
+
+ $('#dateTime').datetimepicker({
+ format: 'Y-m-d H:i',
+ lang: 'ru'
+ });
+
+ makeEditable();
+ }
+ });
+});
diff --git a/src/main/webapp/resources/js/userDatatables.js b/src/main/webapp/resources/js/userDatatables.js
new file mode 100644
index 000000000000..233f33a668d8
--- /dev/null
+++ b/src/main/webapp/resources/js/userDatatables.js
@@ -0,0 +1,76 @@
+var ajaxUrl = 'ajax/admin/users/';
+var datatableApi;
+
+function updateTable() {
+ $.get(ajaxUrl, updateTableByData);
+}
+
+$(function () {
+ datatableApi = $('#datatable').DataTable({
+ "ajax": {
+ "url": ajaxUrl,
+ "dataSrc": ""
+ },
+ "paging": false,
+ "info": true,
+ "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": function (date, type, row) {
+ return renderEditBtn(type, row, 'users.edit');
+ }
+ },
+ {
+ "orderable": false,
+ "defaultContent": "",
+ "render": renderDeleteBtn
+ }
+ ],
+ "order": [
+ [
+ 0,
+ "asc"
+ ]
+ ],
+ "createdRow": function (row, data, dataIndex) {
+ if (!data.enabled) {
+ $(row).css("text-decoration", "line-through");
+ }
+ },
+ "initComplete": makeEditable
+ });
+});
\ 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 000000000000..c42abe696a16
--- /dev/null
+++ b/src/main/webapp/test.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java
new file mode 100644
index 000000000000..01bdba527257
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java
@@ -0,0 +1,44 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.matcher.ModelMatcher;
+import ru.javawebinar.topjava.model.UserMeal;
+import ru.javawebinar.topjava.to.UserMealWithExceed;
+
+import java.time.Month;
+import java.util.Arrays;
+import java.util.List;
+
+import static java.time.LocalDateTime.of;
+import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ;
+
+/**
+ * GKislin
+ * 13.03.2015.
+ */
+public class MealTestData {
+
+ public static final ModelMatcher
MATCHER = new ModelMatcher<>(UserMeal.class);
+ public static final ModelMatcher MATCHER_WITH_EXCEED = new ModelMatcher<>(UserMealWithExceed.class);
+
+ public static final int MEAL1_ID = START_SEQ + 2;
+ public static final int ADMIN_MEAL_ID = START_SEQ + 8;
+
+ public static final UserMeal MEAL1 = new UserMeal(MEAL1_ID, of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500);
+ public static final UserMeal MEAL2 = new UserMeal(MEAL1_ID + 1, of(2015, Month.MAY, 30, 13, 0), "Обед", 1000);
+ public static final UserMeal MEAL3 = new UserMeal(MEAL1_ID + 2, of(2015, Month.MAY, 30, 20, 0), "Ужин", 500);
+ public static final UserMeal MEAL4 = new UserMeal(MEAL1_ID + 3, of(2015, Month.MAY, 31, 10, 0), "Завтрак", 500);
+ public static final UserMeal MEAL5 = new UserMeal(MEAL1_ID + 4, of(2015, Month.MAY, 31, 13, 0), "Обед", 1000);
+ public static final UserMeal MEAL6 = new UserMeal(MEAL1_ID + 5, of(2015, Month.MAY, 31, 20, 0), "Ужин", 510);
+ public static final UserMeal ADMIN_MEAL = new UserMeal(ADMIN_MEAL_ID, of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510);
+ public static final UserMeal ADMIN_MEAL2 = new UserMeal(ADMIN_MEAL_ID + 1, of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500);
+
+ public static final List USER_MEALS = Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1);
+
+ public static UserMeal getCreated() {
+ return new UserMeal(null, of(2015, Month.JUNE, 1, 18, 0), "Созданный ужин", 300);
+ }
+
+ public static UserMeal getUpdated() {
+ return new UserMeal(MEAL1_ID, MEAL1.getDateTime(), "Обновленный завтрак", 200);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java
new file mode 100644
index 000000000000..24657f3132f6
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java
@@ -0,0 +1,39 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.context.support.GenericXmlApplicationContext;
+import ru.javawebinar.topjava.to.UserMealWithExceed;
+import ru.javawebinar.topjava.web.meal.UserMealRestController;
+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;
+
+/**
+ * User: gkislin
+ * Date: 22.08.2014
+ */
+public class SpringMain {
+ public static void main(String[] args) {
+ // java 7 Automatic resource management
+ try (GenericXmlApplicationContext appCtx = new GenericXmlApplicationContext()) {
+ appCtx.getEnvironment().setActiveProfiles(Profiles.ACTIVE_DB, Profiles.DB_IMPLEMENTATION);
+ appCtx.load("spring/spring-app.xml", "spring/spring-db.xml", "spring/spring-mvc.xml");
+ appCtx.refresh();
+
+ System.out.println(Arrays.toString(appCtx.getBeanDefinitionNames()));
+ AdminRestController adminUserController = appCtx.getBean(AdminRestController.class);
+ System.out.println(adminUserController.get(UserTestData.USER_ID));
+ System.out.println();
+
+ UserMealRestController mealController = appCtx.getBean(UserMealRestController.class);
+ List filteredMealsWithExceeded =
+ mealController.getBetween(
+ LocalDate.of(2015, Month.MAY, 30), LocalTime.of(7, 0),
+ LocalDate.of(2015, Month.MAY, 31), LocalTime.of(11, 0));
+ filteredMealsWithExceeded.forEach(System.out::println);
+ }
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/TestUtil.java b/src/test/java/ru/javawebinar/topjava/TestUtil.java
new file mode 100644
index 000000000000..f8284ee0a4fb
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java
@@ -0,0 +1,36 @@
+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.ResultActions;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
+import ru.javawebinar.topjava.matcher.ModelMatcher;
+import ru.javawebinar.topjava.model.User;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * GKislin
+ * 05.01.2015.
+ */
+public class TestUtil {
+
+ public static ResultActions print(ResultActions action) throws UnsupportedEncodingException {
+ System.out.println(getContent(action));
+ return action;
+ }
+
+ public static String getContent(ResultActions action) throws UnsupportedEncodingException {
+ return action.andReturn().getResponse().getContentAsString();
+ }
+
+ public static RequestPostProcessor userHttpBasic(User user) {
+ return SecurityMockMvcRequestPostProcessors.httpBasic(user.getEmail(), user.getPassword());
+ }
+
+ public static void authorize(User user) {
+ SecurityContextHolder.getContext().setAuthentication(
+ new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword()));
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java
new file mode 100644
index 000000000000..1d93c7440a03
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java
@@ -0,0 +1,50 @@
+package ru.javawebinar.topjava;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ru.javawebinar.topjava.matcher.ModelMatcher;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.util.PasswordUtil;
+
+import java.util.Objects;
+
+import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ;
+
+/**
+ * GKislin
+ * 24.09.2015.
+ */
+public class UserTestData {
+ public static final int USER_ID = START_SEQ;
+ public static final int ADMIN_ID = START_SEQ + 1;
+ public static final User USER = new User(USER_ID, "User", "user@yandex.ru", "password", 2005, Role.ROLE_USER);
+ public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", 1900, Role.ROLE_ADMIN, Role.ROLE_USER);
+ private static final Logger LOG = LoggerFactory.getLogger(UserTestData.class);
+ public static final ModelMatcher MATCHER = new ModelMatcher<>(User.class,
+ (expected, actual) -> {
+ if (expected == actual) {
+ return true;
+ }
+ boolean cmp = comparePassword(expected.getPassword(), actual.getPassword())
+ && Objects.equals(expected.getId(), actual.getId())
+ && Objects.equals(expected.getName(), actual.getName())
+ && Objects.equals(expected.getEmail(), actual.getEmail())
+ && Objects.equals(expected.getCaloriesPerDay(), actual.getCaloriesPerDay())
+ && Objects.equals(expected.isEnabled(), actual.isEnabled())
+ && Objects.equals(expected.getRoles(), actual.getRoles());
+ return cmp;
+ }
+ );
+
+
+ private static boolean comparePassword(String rawOrEncodedPassword, String password) {
+ if (PasswordUtil.isEncoded(rawOrEncodedPassword)) {
+ return rawOrEncodedPassword.equals(password);
+ } else if (!PasswordUtil.isMatch(rawOrEncodedPassword, password)) {
+ LOG.error("Password " + password + " doesn't match encoded " + password);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java b/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java
new file mode 100644
index 000000000000..5f8eb2bf884a
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java
@@ -0,0 +1,114 @@
+package ru.javawebinar.topjava.matcher;
+
+import org.junit.Assert;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+import ru.javawebinar.topjava.TestUtil;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+
+/**
+ * GKislin
+ * 06.01.2015.
+ *
+ * @param : entity
+ */
+public class ModelMatcher {
+ private static final EntityComparator DEFAULT_COMPARATOR =
+ (Object expected, Object actual) -> String.valueOf(expected).equals(String.valueOf(actual));
+ protected EntityComparator entityComparator;
+ protected Class entityClass;
+
+ public ModelMatcher(Class entityClass) {
+ this(entityClass, (EntityComparator) DEFAULT_COMPARATOR);
+ }
+
+ public ModelMatcher(Class entityClass, EntityComparator entityComparator) {
+ this.entityClass = entityClass;
+ this.entityComparator = entityComparator;
+ }
+
+ public List> map(Collection collection) {
+ return collection.stream().map(e -> new EntityWrapper<>(e, entityComparator)).collect(Collectors.toList());
+ }
+
+ private T fromJsonValue(String json) {
+ return JsonUtil.readValue(json, entityClass);
+ }
+
+ private Collection fromJsonValues(String json) {
+ return JsonUtil.readValues(json, entityClass);
+ }
+
+ public void assertEquals(T expected, T actual) {
+ Assert.assertEquals(new EntityWrapper<>(expected, entityComparator), new EntityWrapper<>(actual, entityComparator));
+ }
+
+ public ResultMatcher contentMatcher(T expect) {
+ return content().string(
+ new TestMatcher(expect) {
+ @Override
+ protected boolean compare(T expected, String body) {
+ EntityWrapper expectedForCompare = new EntityWrapper<>(expected, entityComparator);
+ EntityWrapper actualForCompare = new EntityWrapper<>(fromJsonValue(body), entityComparator);
+ return expectedForCompare.equals(actualForCompare);
+ }
+ });
+ }
+
+ public final ResultMatcher contentListMatcher(T... expected) {
+ return contentListMatcher(Arrays.asList(expected));
+ }
+
+ public final ResultMatcher contentListMatcher(List expected) {
+ return content().string(new TestMatcher>(expected) {
+ @Override
+ protected boolean compare(List expected, String actual) {
+ List> expectedList = map(expected);
+ List> actualList = map(fromJsonValues(actual));
+ return expectedList.equals(actualList);
+ }
+ });
+ }
+
+ public T fromJsonAction(ResultActions action) throws UnsupportedEncodingException {
+ return fromJsonValue(TestUtil.getContent(action));
+ }
+
+ public void assertCollectionEquals(Collection expected, Collection actual) {
+ Assert.assertEquals(map(expected), map(actual));
+ }
+
+ public interface EntityComparator {
+ boolean compare(T expected, T actual);
+ }
+
+ private static class EntityWrapper {
+ private T entity;
+ private EntityComparator entityComparator;
+
+ private EntityWrapper(T entity, EntityComparator entityComparator) {
+ this.entity = entity;
+ this.entityComparator = entityComparator;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ EntityWrapper that = (EntityWrapper) o;
+ return entity != null ? entityComparator.compare(entity, that.entity) : that.entity == null;
+ }
+
+ @Override
+ public String toString() {
+ return JsonUtil.writeValue(entity);
+ }
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java b/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java
new file mode 100644
index 000000000000..32d599020146
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java
@@ -0,0 +1,29 @@
+package ru.javawebinar.topjava.matcher;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+/**
+ * GKislin
+ * 05.01.2015.
+ */
+abstract public class TestMatcher extends BaseMatcher {
+ protected T expected;
+
+ public TestMatcher(T expected) {
+ this.expected = expected;
+ }
+
+ @Override
+ public boolean matches(Object actual) {
+ return compare(expected, (String) actual);
+ }
+
+ abstract protected boolean compare(T expected, String actual);
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(JsonUtil.writeValue(expected));
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserMealRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserMealRepositoryImpl.java
new file mode 100644
index 000000000000..89fca47e7f76
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserMealRepositoryImpl.java
@@ -0,0 +1,85 @@
+package ru.javawebinar.topjava.repository.mock;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.model.UserMeal;
+import ru.javawebinar.topjava.repository.UserMealRepository;
+import ru.javawebinar.topjava.util.TimeUtil;
+import ru.javawebinar.topjava.util.UserMealsUtil;
+
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import static ru.javawebinar.topjava.UserTestData.ADMIN_ID;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+
+/**
+ * GKislin
+ * 15.09.2015.
+ */
+@Repository
+public class InMemoryUserMealRepositoryImpl implements UserMealRepository {
+
+ private static final Comparator USER_MEAL_COMPARATOR = Comparator.comparing(UserMeal::getDateTime).reversed();
+
+ // Map userId -> (mealId-> meal)
+ private Map> repository = new ConcurrentHashMap<>();
+ private AtomicInteger counter = new AtomicInteger(0);
+
+ {
+ UserMealsUtil.MEAL_LIST.forEach(um -> save(um, USER_ID));
+
+ save(new UserMeal(LocalDateTime.of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510), ADMIN_ID);
+ save(new UserMeal(LocalDateTime.of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500), ADMIN_ID);
+ }
+
+ @Override
+ public UserMeal save(UserMeal userMeal, int userId) {
+ Assert.notNull(userMeal, "userMeal must not be null");
+
+ Integer mealId = userMeal.getId();
+
+ if (userMeal.isNew()) {
+ mealId = counter.incrementAndGet();
+ userMeal.setId(mealId);
+ } else if (get(mealId, userId) == null) {
+ return null;
+ }
+ Map userMeals = repository.computeIfAbsent(userId, ConcurrentHashMap::new);
+ userMeals.put(mealId, userMeal);
+ return userMeal;
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ Map userMeals = repository.get(userId);
+ return userMeals != null && userMeals.remove(id) != null;
+ }
+
+ @Override
+ public UserMeal get(int id, int userId) {
+ Map userMeals = repository.get(userId);
+ return userMeals == null ? null : userMeals.get(id);
+ }
+
+ @Override
+ public Collection getAll(int userId) {
+ Map userMeals = repository.get(userId);
+ return userMeals == null ?
+ Collections.emptyList() :
+ userMeals.values().stream().sorted(USER_MEAL_COMPARATOR).collect(Collectors.toList());
+ }
+
+ @Override
+ public Collection getBetween(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ Assert.notNull(startDateTime, "startDateTime must not be null");
+ Assert.notNull(endDateTime, "endDateTime must not be null");
+ return getAll(userId).stream()
+ .filter(um -> TimeUtil.isBetween(um.getDateTime(), startDateTime, endDateTime))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java
new file mode 100644
index 000000000000..fc117e135f35
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java
@@ -0,0 +1,77 @@
+package ru.javawebinar.topjava.repository.mock;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * GKislin
+ * 15.06.2015.
+ */
+@Repository
+public class InMemoryUserRepositoryImpl implements UserRepository {
+ public static final int USER_ID = 1;
+ public static final int ADMIN_ID = 2;
+ private static final Logger LOG = LoggerFactory.getLogger(InMemoryUserRepositoryImpl.class);
+ private static final Comparator USER_COMPARATOR = Comparator.comparing(User::getName);
+ private Map repository = new ConcurrentHashMap<>();
+ private AtomicInteger counter = new AtomicInteger(0);
+
+ @Override
+ public User save(User user) {
+ Assert.notNull(user, "user must not be null");
+ if (user.isNew()) {
+ user.setId(counter.incrementAndGet());
+ }
+ repository.put(user.getId(), user);
+ return user;
+ }
+
+ @PostConstruct
+ public void postConstruct() {
+ LOG.info("+++ PostConstruct");
+ }
+
+ @PreDestroy
+ public void preDestroy() {
+ LOG.info("+++ PreDestroy");
+ }
+
+ @Override
+ public boolean delete(int id) {
+ return repository.remove(id) != null;
+ }
+
+ @Override
+ public User get(int id) {
+ return repository.get(id);
+ }
+
+ @Override
+ public List getAll() {
+ return repository.values().stream()
+ .sorted(USER_COMPARATOR)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ Assert.notNull(email, "email must not be null");
+ return repository.values().stream()
+ .filter(u -> email.equals(u.getEmail()))
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java
new file mode 100644
index 000000000000..a84a5d19ad20
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java
@@ -0,0 +1,21 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.After;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.repository.JpaUtil;
+
+/**
+ * GKislin
+ * 07.04.2015.
+ */
+abstract public class AbstractJpaUserServiceTest extends AbstractUserServiceTest {
+ @SuppressWarnings("SpringJavaAutowiringInspection")
+ @Autowired
+ private JpaUtil jpaUtil;
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ jpaUtil.clear2ndLevelHibernateCache();
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java
new file mode 100644
index 000000000000..a7895b7ea002
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java
@@ -0,0 +1,50 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.Rule;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.Stopwatch;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.jdbc.SqlConfig;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import java.util.concurrent.TimeUnit;
+
+import static ru.javawebinar.topjava.Profiles.ACTIVE_DB;
+
+/**
+ * User: gkislin
+ */
+@ContextConfiguration({
+ "classpath:spring/spring-app.xml",
+ "classpath:spring/spring-db.xml"
+})
+@RunWith(SpringJUnit4ClassRunner.class)
+@ActiveProfiles(ACTIVE_DB)
+@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8"), executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+abstract public class AbstractServiceTest {
+
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev
+ public Stopwatch stopwatch = new Stopwatch() {
+ private void logInfo(Description description, long nanos) {
+ log.info(String.format("+++ Test %s spent %d microseconds",
+ description.getMethodName(), TimeUnit.NANOSECONDS.toMicros(nanos)));
+ }
+
+ @Override
+ protected void finished(long nanos, Description description) {
+ logInfo(description, nanos);
+ }
+ };
+}
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserMealServiceTest.java
new file mode 100644
index 000000000000..2385804f8eb7
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserMealServiceTest.java
@@ -0,0 +1,77 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.model.UserMeal;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.Arrays;
+
+import static ru.javawebinar.topjava.MealTestData.*;
+import static ru.javawebinar.topjava.UserTestData.ADMIN_ID;
+import static ru.javawebinar.topjava.UserTestData.USER_ID;
+
+abstract public class AbstractUserMealServiceTest extends AbstractServiceTest {
+
+ @Autowired
+ protected UserMealService service;
+
+ @Test
+ public void testDelete() throws Exception {
+ service.delete(MEAL1_ID, USER_ID);
+ MATCHER.assertCollectionEquals(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2), service.getAll(USER_ID));
+ }
+
+ @Test
+ public void testDeleteNotFound() throws Exception {
+ thrown.expect(NotFoundException.class);
+ service.delete(MEAL1_ID, 1);
+ }
+
+ @Test
+ public void testSave() throws Exception {
+ UserMeal created = getCreated();
+ service.save(created, USER_ID);
+ MATCHER.assertCollectionEquals(Arrays.asList(created, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1), service.getAll(USER_ID));
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ UserMeal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID);
+ MATCHER.assertEquals(ADMIN_MEAL, actual);
+ }
+
+ @Test
+ public void testGetNotFound() throws Exception {
+ thrown.expect(NotFoundException.class);
+ service.get(MEAL1_ID, ADMIN_ID);
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ UserMeal updated = getUpdated();
+ service.update(updated, USER_ID);
+ MATCHER.assertEquals(updated, service.get(MEAL1_ID, USER_ID));
+ }
+
+ @Test
+ public void testNotFoundUpdate() throws Exception {
+ UserMeal item = service.get(MEAL1_ID, USER_ID);
+ thrown.expect(NotFoundException.class);
+ thrown.expectMessage("Not found entity with id=" + MEAL1_ID);
+ service.update(item, ADMIN_ID);
+ }
+
+ @Test
+ public void testGetAll() throws Exception {
+ MATCHER.assertCollectionEquals(USER_MEALS, service.getAll(USER_ID));
+ }
+
+ @Test
+ public void testGetBetween() throws Exception {
+ MATCHER.assertCollectionEquals(Arrays.asList(MEAL3, MEAL2, MEAL1),
+ service.getBetweenDates(LocalDate.of(2015, Month.MAY, 30), LocalDate.of(2015, Month.MAY, 30), USER_ID));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java
new file mode 100644
index 000000000000..29b59e017246
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java
@@ -0,0 +1,83 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.After;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import static ru.javawebinar.topjava.UserTestData.*;
+
+abstract public class AbstractUserServiceTest extends AbstractServiceTest {
+
+ @Autowired
+ protected UserService service;
+
+ @After
+ public void tearDown() throws Exception {
+ service.evictCache();
+ }
+
+ @Test
+ public void testSave() throws Exception {
+ User newUser = new User(null, "New", "new@gmail.com", "newPass", 1555, false, Collections.singleton(Role.ROLE_USER));
+ User created = service.save(newUser);
+ newUser.setId(created.getId());
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, newUser, USER), service.getAll());
+ }
+
+ @Test(expected = DataAccessException.class)
+ public void testDuplicateMailSave() throws Exception {
+ service.save(new User(null, "Duplicate", "user@yandex.ru", "newPass", 1000, Role.ROLE_USER));
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ service.delete(USER_ID);
+ MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), service.getAll());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testNotFoundDelete() throws Exception {
+ service.delete(1);
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ User user = service.get(USER_ID);
+ MATCHER.assertEquals(USER, user);
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetNotFound() throws Exception {
+ service.get(1);
+ }
+
+ @Test
+ public void testGetByEmail() throws Exception {
+ User user = service.getByEmail("user@yandex.ru");
+ MATCHER.assertEquals(USER, user);
+ }
+
+ @Test
+ public void testGetAll() throws Exception {
+ Collection all = service.getAll();
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, USER), all);
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ User updated = new User(USER);
+ updated.setName("UpdatedName");
+ updated.setCaloriesPerDay(330);
+ updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN));
+ service.update(updated);
+ MATCHER.assertEquals(updated, service.get(USER_ID));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserMealServiceTest.java
new file mode 100644
index 000000000000..0b85be7188e8
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserMealServiceTest.java
@@ -0,0 +1,27 @@
+package ru.javawebinar.topjava.service.datajpa;
+
+import org.junit.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.UserMeal;
+import ru.javawebinar.topjava.service.AbstractUserMealServiceTest;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.MealTestData.*;
+import static ru.javawebinar.topjava.Profiles.DATAJPA;
+import static ru.javawebinar.topjava.UserTestData.ADMIN_ID;
+
+@ActiveProfiles(DATAJPA)
+public class DataJpaUserMealServiceTest extends AbstractUserMealServiceTest {
+ @Test
+ public void testGetWithUser() throws Exception {
+ UserMeal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID);
+ MATCHER.assertEquals(ADMIN_MEAL, adminMeal);
+ UserTestData.MATCHER.assertEquals(UserTestData.ADMIN, adminMeal.getUser());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetWithUserNotFound() throws Exception {
+ service.getWithUser(MEAL1_ID, ADMIN_ID);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java
new file mode 100644
index 000000000000..5d67dce09e59
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java
@@ -0,0 +1,26 @@
+package ru.javawebinar.topjava.service.datajpa;
+
+import org.junit.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.MealTestData;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.Profiles.DATAJPA;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+@ActiveProfiles(DATAJPA)
+public class DataJpaUserServiceTest extends AbstractJpaUserServiceTest {
+ @Test
+ public void testGetWithMeals() throws Exception {
+ User user = service.getWithMeals(USER_ID);
+ MATCHER.assertEquals(USER, user);
+ MealTestData.MATCHER.assertCollectionEquals(MealTestData.USER_MEALS, user.getMeals());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetWithMealsNotFound() throws Exception {
+ service.getWithMeals(1);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserMealServiceTest.java
new file mode 100644
index 000000000000..d310717c67a5
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserMealServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jdbc;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractUserMealServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JDBC;
+
+@ActiveProfiles(JDBC)
+public class JdbcUserMealServiceTest extends AbstractUserMealServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java
new file mode 100644
index 000000000000..419f68ed1098
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jdbc;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractUserServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JDBC;
+
+@ActiveProfiles(JDBC)
+public class JdbcUserServiceTest extends AbstractUserServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserMealServiceTest.java
new file mode 100644
index 000000000000..778235ed8ef0
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserMealServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jpa;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractUserMealServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JPA;
+
+@ActiveProfiles(JPA)
+public class JpaUserMealServiceTest extends AbstractUserMealServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java
new file mode 100644
index 000000000000..ffb1fea03c22
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jpa;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JPA;
+
+@ActiveProfiles(JPA)
+public class JpaUserServiceTest extends AbstractJpaUserServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java
new file mode 100644
index 000000000000..d47e6d0e1c9f
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java
@@ -0,0 +1,68 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.filter.CharacterEncodingFilter;
+import ru.javawebinar.topjava.repository.JpaUtil;
+import ru.javawebinar.topjava.service.UserService;
+
+import javax.annotation.PostConstruct;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static ru.javawebinar.topjava.Profiles.ACTIVE_DB;
+import static ru.javawebinar.topjava.Profiles.DB_IMPLEMENTATION;
+
+/**
+ * User: gkislin
+ * Date: 10.08.2014
+ */
+@ContextConfiguration({
+ "classpath:spring/spring-app.xml",
+ "classpath:spring/spring-mvc.xml",
+ "classpath:spring/spring-db.xml"
+})
+@WebAppConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+@Transactional
+@ActiveProfiles({ACTIVE_DB, DB_IMPLEMENTATION})
+abstract public class AbstractControllerTest {
+
+ private static final CharacterEncodingFilter CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter();
+
+ static {
+ CHARACTER_ENCODING_FILTER.setEncoding("UTF-8");
+ CHARACTER_ENCODING_FILTER.setForceEncoding(true);
+ }
+
+ protected MockMvc mockMvc;
+ @Autowired
+ protected UserService userService;
+ @Autowired
+ private JpaUtil jpaUtil;
+ @Autowired
+ private WebApplicationContext webApplicationContext;
+
+ @PostConstruct
+ private void postConstruct() {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(webApplicationContext)
+ .addFilter(CHARACTER_ENCODING_FILTER)
+ .apply(springSecurity())
+ .build();
+ }
+
+ @Before
+ public void setUp() {
+ userService.evictCache();
+ jpaUtil.clear2ndLevelHibernateCache();
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerSpringTest.java
new file mode 100644
index 000000000000..ca416455051c
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerSpringTest.java
@@ -0,0 +1,54 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+import ru.javawebinar.topjava.web.user.AdminRestController;
+
+import java.util.Collection;
+
+import static ru.javawebinar.topjava.UserTestData.ADMIN;
+import static ru.javawebinar.topjava.UserTestData.USER;
+
+/**
+ * GKislin
+ * 13.03.2015.
+ */
+@ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/spring-mvc.xml", "classpath:spring/mock.xml"})
+@RunWith(SpringJUnit4ClassRunner.class)
+public class InMemoryAdminRestControllerSpringTest {
+
+ @Autowired
+ private AdminRestController controller;
+
+ @Autowired
+ private UserRepository repository;
+
+ @Before
+ public void setUp() throws Exception {
+ repository.getAll().forEach(u -> repository.delete(u.getId()));
+ repository.save(USER);
+ repository.save(ADMIN);
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ controller.delete(UserTestData.USER_ID);
+ Collection users = controller.getAll();
+ Assert.assertEquals(users.size(), 1);
+ Assert.assertEquals(users.iterator().next(), ADMIN);
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testDeleteNotFound() throws Exception {
+ controller.delete(10);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerTest.java
new file mode 100644
index 000000000000..69466f713308
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/InMemoryAdminRestControllerTest.java
@@ -0,0 +1,57 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.*;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+import ru.javawebinar.topjava.web.user.AdminRestController;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import static ru.javawebinar.topjava.UserTestData.ADMIN;
+import static ru.javawebinar.topjava.UserTestData.USER;
+
+public class InMemoryAdminRestControllerTest {
+ private static ConfigurableApplicationContext appCtx;
+ private static AdminRestController controller;
+
+ @BeforeClass
+ public static void beforeClass() {
+ appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/spring-mvc.xml", "spring/mock.xml");
+ System.out.println("\n" + Arrays.toString(appCtx.getBeanDefinitionNames()) + "\n");
+ controller = appCtx.getBean(AdminRestController.class);
+ }
+
+ @AfterClass
+ public static void afterClass() {
+// May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed
+// http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite
+// appCtx.close();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ // Re-initialize
+ UserRepository repository = appCtx.getBean(UserRepository.class);
+ repository.getAll().forEach(u -> repository.delete(u.getId()));
+ repository.save(USER);
+ repository.save(ADMIN);
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ controller.delete(UserTestData.USER_ID);
+ Collection users = controller.getAll();
+ Assert.assertEquals(users.size(), 1);
+ Assert.assertEquals(users.iterator().next(), ADMIN);
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testDeleteNotFound() throws Exception {
+ controller.delete(10);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java
new file mode 100644
index 000000000000..3d2585a50973
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java
@@ -0,0 +1,24 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.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;
+
+/**
+ * GKislin
+ * 10.04.2015.
+ */
+public class ResourceControllerTest extends AbstractControllerTest {
+
+ @Test
+ public void testResources() throws Exception {
+ mockMvc.perform(get("/resources/css/style.css"))
+ .andDo(print())
+ .andExpect(content().contentType(MediaType.valueOf("text/css")))
+ .andExpect(status().isOk());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java
new file mode 100644
index 000000000000..d1a7589c42ea
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java
@@ -0,0 +1,30 @@
+package ru.javawebinar.topjava.web.json;
+
+import org.junit.Test;
+import ru.javawebinar.topjava.MealTestData;
+import ru.javawebinar.topjava.model.UserMeal;
+
+import java.util.List;
+
+/**
+ * GKislin
+ * 22.07.2015.
+ */
+public class JsonUtilTest {
+
+ @Test
+ public void testReadWriteValue() throws Exception {
+ String json = JsonUtil.writeValue(MealTestData.ADMIN_MEAL);
+ System.out.println(json);
+ UserMeal userMeal = JsonUtil.readValue(json, UserMeal.class);
+ MealTestData.MATCHER.assertEquals(MealTestData.ADMIN_MEAL, userMeal);
+ }
+
+ @Test
+ public void testReadWriteValues() throws Exception {
+ String json = JsonUtil.writeValue(MealTestData.USER_MEALS);
+ System.out.println(json);
+ List userMeals = JsonUtil.readValues(json, UserMeal.class);
+ MealTestData.MATCHER.assertCollectionEquals(MealTestData.USER_MEALS, userMeals);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/UserMealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/UserMealRestControllerTest.java
new file mode 100644
index 000000000000..91535d13d719
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/meal/UserMealRestControllerTest.java
@@ -0,0 +1,142 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.ResultActions;
+import ru.javawebinar.topjava.model.UserMeal;
+import ru.javawebinar.topjava.service.UserMealService;
+import ru.javawebinar.topjava.util.UserMealsUtil;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.assertEquals;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javawebinar.topjava.MealTestData.*;
+import static ru.javawebinar.topjava.MealTestData.MATCHER;
+import static ru.javawebinar.topjava.TestUtil.userHttpBasic;
+import static ru.javawebinar.topjava.UserTestData.*;
+import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ;
+
+public class UserMealRestControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL = UserMealRestController.REST_URL + '/';
+
+ @Autowired
+ private UserMealService service;
+
+ @Test
+ public void testGet() throws Exception {
+ mockMvc.perform(get(REST_URL + ADMIN_MEAL_ID)
+ .with(userHttpBasic(ADMIN)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentMatcher(ADMIN_MEAL));
+ }
+
+ @Test
+ public void testGetUnauth() throws Exception {
+ mockMvc.perform(get(REST_URL + MEAL1_ID))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testGetNotFound() throws Exception {
+ mockMvc.perform(get(REST_URL + ADMIN_MEAL_ID)
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testDeleteNotFound() throws Exception {
+ mockMvc.perform(delete(REST_URL + ADMIN_MEAL_ID)
+ .with(userHttpBasic(USER)))
+ .andDo(print())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ mockMvc.perform(delete(REST_URL + MEAL1_ID)
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isOk());
+ MATCHER.assertCollectionEquals(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2), service.getAll(START_SEQ));
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ UserMeal updated = getUpdated();
+
+ mockMvc.perform(put(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(updated))
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isOk());
+
+ assertEquals(updated, service.get(MEAL1_ID, START_SEQ));
+ }
+
+ @Test
+ public void testCreate() throws Exception {
+ UserMeal created = getCreated();
+ ResultActions action = mockMvc.perform(post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(created))
+ .with(userHttpBasic(ADMIN)));
+
+ UserMeal returned = MATCHER.fromJsonAction(action);
+ created.setId(returned.getId());
+
+ MATCHER.assertEquals(created, returned);
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN_MEAL2, created, ADMIN_MEAL), service.getAll(ADMIN_ID));
+ }
+
+ @Test
+ public void testGetAll() throws Exception {
+ mockMvc.perform(get(REST_URL)
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER_WITH_EXCEED.contentListMatcher(UserMealsUtil.getWithExceeded(USER_MEALS, USER.getCaloriesPerDay())));
+ }
+
+ @Test
+ public void testGetBetween() throws Exception {
+ mockMvc.perform(get(REST_URL + "between?startDateTime=2015-05-30T07:00&endDateTime=2015-05-31T11:00:00")
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(MATCHER_WITH_EXCEED.contentListMatcher(
+ UserMealsUtil.createWithExceed(MEAL4, true),
+ UserMealsUtil.createWithExceed(MEAL1, false)));
+ }
+
+ @Test
+ public void testFilter() throws Exception {
+ mockMvc.perform(get(REST_URL + "filter")
+ .param("startDate", "2015-05-30").param("startTime", "07:00")
+ .param("endDate", "2015-05-31").param("endTime", "11:00")
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(MATCHER_WITH_EXCEED.contentListMatcher(
+ UserMealsUtil.createWithExceed(MEAL4, true),
+ UserMealsUtil.createWithExceed(MEAL1, false)));
+ }
+
+ @Test
+ public void testFilterAll() throws Exception {
+ mockMvc.perform(get(REST_URL + "filter?startDate=&endTime=")
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(MATCHER_WITH_EXCEED.contentListMatcher(
+ UserMealsUtil.getWithExceeded(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1), 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 000000000000..726aafd99324
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java
@@ -0,0 +1,121 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.ResultActions;
+import ru.javawebinar.topjava.TestUtil;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javawebinar.topjava.TestUtil.userHttpBasic;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+// https://jira.spring.io/browse/SPR-14472
+public class AdminRestControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL = AdminRestController.REST_URL + '/';
+
+ @Test
+ public void testGet() throws Exception {
+ mockMvc.perform(get(REST_URL + ADMIN_ID)
+ .with(userHttpBasic(ADMIN)))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentMatcher(ADMIN));
+ }
+
+ @Test
+ public void testGetNotFound() throws Exception {
+ mockMvc.perform(get(REST_URL + 1)
+ .with(TestUtil.userHttpBasic(ADMIN)))
+ .andExpect(status().isNotFound())
+ .andDo(print());
+ }
+
+ @Test
+ public void testGetByEmail() throws Exception {
+ mockMvc.perform(get(REST_URL + "by?email=" + ADMIN.getEmail())
+ .with(userHttpBasic(ADMIN)))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentMatcher(ADMIN));
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ mockMvc.perform(delete(REST_URL + USER_ID)
+ .with(userHttpBasic(ADMIN)))
+ .andDo(print())
+ .andExpect(status().isOk());
+ MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), userService.getAll());
+ }
+
+ @Test
+ public void testDeleteNotFound() throws Exception {
+ mockMvc.perform(delete(REST_URL + 1)
+ .with(TestUtil.userHttpBasic(ADMIN)))
+ .andExpect(status().isNotFound())
+ .andDo(print());
+ }
+
+ @Test
+ public void testGetUnauth() throws Exception {
+ mockMvc.perform(get(REST_URL))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testGetForbidden() throws Exception {
+ mockMvc.perform(get(REST_URL)
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ User updated = new User(USER);
+ updated.setName("UpdatedName");
+ updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN));
+ mockMvc.perform(put(REST_URL + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(ADMIN))
+ .content(JsonUtil.writeValue(updated)))
+ .andExpect(status().isOk());
+
+ MATCHER.assertEquals(updated, userService.get(USER_ID));
+ }
+
+ @Test
+ public void testCreate() throws Exception {
+ User expected = new User(null, "New", "new@gmail.com", "newPass", 1000, Role.ROLE_USER, Role.ROLE_ADMIN);
+ ResultActions action = mockMvc.perform(post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(ADMIN))
+ .content(JsonUtil.writeValue(expected))).andExpect(status().isCreated());
+
+ User returned = MATCHER.fromJsonAction(action);
+ expected.setId(returned.getId());
+
+ MATCHER.assertEquals(expected, returned);
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, expected, USER), userService.getAll());
+ }
+
+ @Test
+ public void testGetAll() throws Exception {
+ TestUtil.print(mockMvc.perform(get(REST_URL)
+ .with(userHttpBasic(ADMIN)))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentListMatcher(ADMIN, USER)));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/HerokuRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/HerokuRestControllerTest.java
new file mode 100644
index 000000000000..c0af34211f2e
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/HerokuRestControllerTest.java
@@ -0,0 +1,62 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.Test;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.ResourcePropertySource;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.io.IOException;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javawebinar.topjava.Profiles.DB_IMPLEMENTATION;
+import static ru.javawebinar.topjava.Profiles.HEROKU;
+import static ru.javawebinar.topjava.TestUtil.userHttpBasic;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+@ActiveProfiles({HEROKU, DB_IMPLEMENTATION})
+public class HerokuRestControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL = AdminRestController.REST_URL + '/';
+
+ // Set DATABASE_URL environment for heroku profile
+ static {
+ Resource resource = new ClassPathResource("db/postgres.properties");
+ try {
+ PropertySource propertySource = new ResourcePropertySource(resource);
+ String herokuDbUrl = String.format("postgres://%s:%s@%s",
+ propertySource.getProperty("database.username"),
+ propertySource.getProperty("database.password"),
+ ((String) propertySource.getProperty("database.url")).substring(18));
+ System.out.println(herokuDbUrl);
+
+ System.setProperty("DATABASE_URL", herokuDbUrl);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ mockMvc.perform(delete(REST_URL + USER_ID)
+ .with(userHttpBasic(ADMIN)))
+ .andDo(print())
+ .andExpect(status().is5xxServerError());
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ mockMvc.perform(put(REST_URL + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(ADMIN))
+ .content(JsonUtil.writeValue(USER)))
+ .andExpect(status().is5xxServerError());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java
new file mode 100644
index 000000000000..2d773c121cf8
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java
@@ -0,0 +1,72 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.Test;
+import org.springframework.http.MediaType;
+import ru.javawebinar.topjava.TestUtil;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.UserUtil;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.util.Collections;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static ru.javawebinar.topjava.TestUtil.userHttpBasic;
+import static ru.javawebinar.topjava.UserTestData.*;
+import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL;
+
+public class ProfileRestControllerTest extends AbstractControllerTest {
+
+ @Test
+ public void testGet() throws Exception {
+ TestUtil.print(mockMvc.perform(get(REST_URL)
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+ .andExpect(MATCHER.contentMatcher(USER)));
+ }
+
+ @Test
+ public void testGetUnauth() throws Exception {
+ mockMvc.perform(get(REST_URL))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ mockMvc.perform(delete(REST_URL)
+ .with(userHttpBasic(USER)))
+ .andExpect(status().isOk());
+ MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), userService.getAll());
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ UserTo updatedTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500);
+
+ mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(USER))
+ .content(JsonUtil.writeValue(updatedTo)))
+ .andDo(print())
+ .andExpect(status().isOk());
+
+ User expected = new User(USER);
+ UserUtil.updateFromTo(expected, updatedTo);
+ MATCHER.assertEquals(expected, userService.getByEmail("newemail@ya.ru"));
+ }
+
+ @Test
+ public void testUpdateNotValid() throws Exception {
+ UserTo updatedTo = new UserTo(null, null, "password", null, 1500);
+
+ mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON)
+ .with(userHttpBasic(USER))
+ .content(JsonUtil.writeValue(updatedTo)))
+ .andDo(print())
+ .andExpect(status().isBadRequest());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/user/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/RootControllerTest.java
new file mode 100644
index 000000000000..314028e8afeb
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/RootControllerTest.java
@@ -0,0 +1,47 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.junit.Test;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+
+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.authorize;
+import static ru.javawebinar.topjava.UserTestData.ADMIN;
+import static ru.javawebinar.topjava.UserTestData.USER;
+
+/**
+ * GKislin
+ * 10.04.2015.
+ */
+public class RootControllerTest extends AbstractControllerTest {
+
+ @Test
+ public void testUserList() throws Exception {
+ authorize(ADMIN);
+ mockMvc.perform(get("/users"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(view().name("userList"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/userList.jsp"));
+ }
+
+ @Test
+ public void testUserListUnAuth() throws Exception {
+ authorize(USER);
+ mockMvc.perform(get("/users"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(view().name("exception/exception"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/exception/exception.jsp"));
+ }
+
+ @Test
+ public void testMealList() throws Exception {
+ authorize(USER);
+ mockMvc.perform(get("/meals"))
+ .andDo(print())
+ .andExpect(view().name("mealList"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/mealList.jsp"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 000000000000..661579943554
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,21 @@
+
+
+
+ true
+
+
+
+
+ UTF-8
+ %-5level %logger{0} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/resources/spring/mock.xml b/src/test/resources/spring/mock.xml
new file mode 100644
index 000000000000..7d91f0ea5943
--- /dev/null
+++ b/src/test/resources/spring/mock.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/system.properties b/system.properties
new file mode 100644
index 000000000000..916c446bb77d
--- /dev/null
+++ b/system.properties
@@ -0,0 +1 @@
+java.runtime.version=1.8
\ No newline at end of file