diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000000..6b280b2d00f4
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,13 @@
+# https://docs.travis-ci.com/user/languages/java/
+language: java
+jdk: oraclejdk8
+
+#https://dzone.com/articles/travis-ci-tutorial-java-projects
+cache:
+ directories:
+ - $HOME/.m2
+
+# https://docs.travis-ci.com/user/database-setup/#PostgreSQL
+before_script:
+ - psql -c 'create database topjava' -U postgres
+ - psql -c 'create user "user"; grant all privileges on database topjava to "user"' -U postgres
\ No newline at end of file
diff --git a/Procfile b/Procfile
new file mode 100644
index 000000000000..4afeb7900918
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: java $JAVA_OPTS -Dspring.profiles.active="datajpa,heroku" -DTOPJAVA_ROOT="." -jar target/dependency/webapp-runner.jar --port $PORT target/*.war
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 000000000000..0c9156d3756d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,126 @@
+Java Enterprise Online Project
+===============================
+Наиболее востребованные технологии /инструменты / фреймворки Java Enterprise:
+Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + plugins.
+
+ Когда вы слышите что-то, вы забываете это.
+ Когда вы видите что-то, вы запоминаете это.
+ Но только когда вы начинаете делать это,
+ вы начинаете понимать это
+
+ Старинная китайская поговорка
+
+## Описание и план проекта
+### Демо разрабатываемого приложения
+### Требования к участникам, Wiki
+### Составление резюме, подготовка к интервью, поиск работы
+
+Вводное занятие
+===============
+##  Системы управления версиями. Git.
+- ** Wiki по ведению проекта в Git **
+- Система управления версиями . VCS/DVSC .
+- Ресурсы:
+ - Интерактивная Git обучалка
+ - Книга Git
+ - Working with remote repositories
+ - Видео по обучению Git
+ - Git Overview
+ - Видеокурс по Git
+
+##  Работа с проектом (выполнять инструкции)
+- **Prepare_ to_ HW0.patch (скачать и положить в каталог вашего проекта) **
+- Доступно о Java 8 Lambda
+
+## Инструкция по шагам (из видео):
+- Установить ПО (git, JDK8, IntelliJ IDEA, Maven)
+- Создать аккаунт на GitHub
+- Сделать Fork ЭТОГО проекта (https://github.com/JavaOPs/topjava)
+- Сделать локальный репозиторий проекта:
+
git clone https://github.com/[Ваш аккаунт]/topjava.git
+- Открыть и настроить проект в IDEA
+ - Выставить кодировку UTF-8 в консоли
+ - Поставить кодировку UTF-8
+ - Поменять фонт по умолчанию (DejaVu)
+- По ходу видео сделать Apply Patch... скаченного патча Prepare_ to_ HW0.patch
+- Закоммитить и запушить изменения (commit + push)
+- Сделать ветку домашнего задания
+- Выполнить задание и залить на GitHub (commit + push)
+- Переключиться в основную ветку проекта master.
+
+##  Домашнее задание HW0
+
+- Java 8 Date and Time API
+- Алгоритмы и структуры данных для начинающих: сложность алгоритмов
+- Time complexity
+- Временная сложность алгоритма
+- Вычислительная сложность
+```
+Реализовать метод UserMealsUtil.getFilteredWithExceeded:
+- должны возвращаться только записи между startTime и endTime
+- поле UserMealWithExceed.exceed должно показывать,
+ превышает ли сумма калорий за весь день параметра метода caloriesPerDay
+
+Т.е UserMealWithExceed - это запись одной еды, но поле exceeded будет одинаково для всех записей за этот день.
+
+- Проверте результат выполнения ДЗ (можно проверить логику в http://topjava.herokuapp.com , список еды)
+- Оцените Time complexity вашего алгоритма, если он O(N*N)- попробуйте сделать O(N).
+```
+Optional
+
+- Java 8: Lambda выражения
+- Java 8: Потоки
+- Pуководство по Java 8 Stream
+- Лямбда-выражения в Java 8
+- A Guide to Java 8
+- Шпаргалка Java Stream API
+- Алексея Владыкин: Элементы функционального программирования в Java
+- stream.map vs forEach
+
+```
+Сделать реализация через Java 8 Stream API. Заменяйте forEach оператором stream.map(..)
+```
+### Полезные ресурсы
+> ВНИМАНИЕ: основы JavaSсript необходимы для понимания проекта, начиная с 8-го занятия!
+
+Все остальное - опционально.
+
+#### HTML, JavaScript, CSS
+- Введение в программирование на JavaScript
+- Основы работы с HTML/CSS/JavaScript
+- JavaScript - Основы
+- Bootstrap 3 - Основы
+- jQuery для начинающих
+
+#### Java (базовые вещи)
+- Интуит. Программирование на Java
+- Основы Java garbage collection
+- Размер Java объектов
+- Блог о памяти, сборщике мусора, многопоточности и производительности в JAVA
+- Введение в Java Reflection API
+- Структуры данных в картинках
+- Обзор java.util.concurrent.*
+- Синхронизация потоков
+- String literal pool
+- Маленькие хитрости Java
+- Как создать Servlet? Полное руководство.
+- A Guide to Java 8
+
+#### JDBC, SQL
+- Уроки по JDBC
+- Learn SQL
+- Try SQL
+- Курс "Введение в базы данных"
+
+#### Разное
+- Эффективная работа с кодом в IntelliJ IDEA
+- Quizful- тесты онлайн
+- Введение в Linux
+
+#### Книги
+- Джошуа Блох: Java. Эффективное программирование. Второе издание
+- Гамма, Хелм, Джонсон: Приемы объектно-ориентированного проектирования. Паттерны проектирования
+- Редмонд Э.: Семь баз данных за семь недель. Введение в современные базы данных и идеологию NoSQL
+- Brian Goetz: Java Concurrency in Practice
+- G.L. McDowell: Cracking the Coding Interview
diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml
new file mode 100644
index 000000000000..6ead35a225a6
--- /dev/null
+++ b/config/Topjava-soapui-project.xml
@@ -0,0 +1,633 @@
+
+
+
+
+
+
+
+ http://localhost:8080
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 500 401
+
+ html
+
+
+ application/json
+ 200
+
+ ns:Response
+
+
+
+ 0
+
+ data
+
+
+ application/json;charset=UTF-8
+ 200
+
+ Response
+
+
+
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/admin/users
+
+ user@yandex.ru
+ password
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+ application/json
+
+
+
+ text/html;charset=utf-8
+ 500
+
+ html
+
+
+ application/json
+ 201
+
+ user:Response
+
+
+ application/json;charset=UTF-8
+ 201
+
+ user:Response
+
+
+
+
+
+ http://localhost:8080
+ {
+ "name": "New2",
+ "email": "new2@yandex.ru",
+ "password": "passwordNew",
+ "roles": ["ROLE_USER"]
+ }
+
+ http://localhost/topjava/rest/admin/users
+
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 500
+
+ html
+
+
+ application/json
+ 200
+
+ ns:Response
+
+
+ application/json;charset=UTF-8
+ 200
+
+ ns:Response
+
+
+
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/admin/users/100000
+
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 405 500
+
+ html
+
+
+ application/json
+
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+
+
+ http://localhost:8080
+ {
+ "name": "UserUpdated",
+ "email": "user@yandex.ru",
+ "password": "passwordNew",
+ "roles": ["ROLE_USER"]
+ }
+
+ http://localhost/topjava/rest/admin/users/100000
+
+ Basic
+ Basic
+ Global HTTP Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 500
+
+ html
+
+
+ application/json
+ 200
+
+ ns:Response
+
+
+ application/json;charset=UTF-8
+ 200
+
+ prof:Response
+
+
+
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile
+
+ Basic
+ Basic
+ Global HTTP Settings
+
+
+
+
+
+
+
+
+
+
+ application/json
+
+
+
+ text/html;charset=utf-8
+ 500 405
+
+ html
+
+
+ application/json
+ 201
+
+ user:Response
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+
+
+ http://localhost:8080
+ {
+ "name": "New777",
+ "email": "new777@yandex.ru",
+ "password": "passwordNew",
+ "roles": ["ROLE_USER"]
+ }
+
+ http://localhost/topjava/rest/profile
+
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+ 200
+
+ data
+
+
+ text/html;charset=utf-8
+ 500
+
+ html
+
+
+ application/json
+
+
+
+
+ 200
+
+ data
+
+
+
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile
+
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ application/json;charset=UTF-8
+ 200
+
+ Response
+
+
+
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile/meals
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 400
+
+ html
+
+
+ application/json
+
+
+
+
+ 0
+
+ data
+
+
+ application/json;charset=UTF-8
+ 201
+
+ meal:Response
+
+
+
+
+
+ http://localhost:8080
+ {
+ "dateTime": "2015-06-01 10:00",
+ "description": "Новый завтрак",
+ "calories": 777
+ }
+
+ http://localhost/topjava/rest/profile/meals
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ application/json;charset=UTF-8
+ 200
+
+ ns:Response
+
+
+
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile/meals/100002
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 400
+
+ html
+
+
+ application/json
+
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+
+
+ http://localhost:8080
+ {
+ "id": 100002,
+ "dateTime": "2015-05-30 10:00",
+ "description": "Обновленный завтрак",
+ "calories": 500
+ }
+
+ http://localhost/topjava/rest/profile/meals/100002
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+ startDateTime
+
+ QUERY
+
+
+
+
+ endDateTime
+
+ QUERY
+
+
+
+
+
+
+
+
+ application/json;charset=UTF-8
+ 200
+
+ Response
+
+
+
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile/meals/between
+
+ No Authorization
+
+
+
+
+
+
+
+
+ startDateTime
+ endDateTime
+
+
+
+
+
+
+
+
+ startDate
+
+ QUERY
+
+
+
+ endTime QUERY
+
+
+
+
+ application/json;charset=UTF-8
+ 200
+
+ Response
+
+
+
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile/meals/filter
+
+ No Authorization
+
+
+
+
+
+
+
+
+ startDate endTime
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config/curl.md b/config/curl.md
new file mode 100644
index 000000000000..6fc4b3f7f43c
--- /dev/null
+++ b/config/curl.md
@@ -0,0 +1,23 @@
+### Test MealRestController (application deployed in application context `topjava`).
+> For windows use `Git Bash`
+
+#### get All
+`curl -s http://localhost:8080/topjava/rest/profile/meals`
+
+#### get 100003
+`curl -s http://localhost:8080/topjava/rest/profile/meals/100003`
+
+#### get between
+`curl -s "http://localhost:8080/topjava/rest/profile/meals/between?startDateTime=2015-05-30T08:00:00&endDateTime=2015-05-30T16:00:00"`
+
+#### get not found
+`curl -s -v http://localhost:8080/rest/meals/100008`
+
+#### delete
+`curl -s -X DELETE http://localhost:8080/rest/meals/100002`
+
+#### create
+`curl -s -v -X POST -d '{"dateTime":"2015-06-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals`
+
+#### update
+`curl -s -v -X PUT -d '{"dateTime":"2015-05-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003`
\ No newline at end of file
diff --git a/config/messages/app.properties b/config/messages/app.properties
new file mode 100644
index 000000000000..37fa6faefdc8
--- /dev/null
+++ b/config/messages/app.properties
@@ -0,0 +1,46 @@
+app.title=Calories management
+app.home=Home
+app.footer=Project Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery application
+app.login=Login
+app.logout=Logout
+app.profile=profile
+app.register=Register
+app.registered=You are registered. Please Sign in.
+
+users.title=User list
+users.edit=Edit user
+users.add=Add user
+users.name=Name
+users.email=Email
+users.roles=Roles
+users.active=Active
+users.registered=Registered
+users.password=Password
+users.caloriesPerDay=Calories per day
+
+meals.title=Meals list
+meals.edit=Edit meal
+meals.add=Add meal
+meals.filter=Filter
+meals.startDate=From date
+meals.endDate=To date
+meals.startTime=From time
+meals.endTime=To time
+meals.description=Description
+meals.dateTime=DateTime
+meals.calories=Calories
+
+common.select=Select
+common.delete=Delete
+common.update=Update
+common.save=Save
+common.search=Search
+
+common.deleted=Deleted
+common.saved=Saved
+common.enabled=Enabled
+common.disabled=Disabled
+common.failed=Failed
+common.add=Add
+
+exception.duplicate_email=User with this email already present in application.
\ No newline at end of file
diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties
new file mode 100644
index 000000000000..d75e0a32590c
--- /dev/null
+++ b/config/messages/app_ru.properties
@@ -0,0 +1,46 @@
+app.title=Подсчет калорий
+app.home=Главная
+app.footer=Приложение по проекту Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery
+app.login=Войти
+app.logout=Выйти
+app.profile=профиль
+app.register=Зарегистрироваться
+app.registered=Вы загеристрированы. Введите ваш логин/пароль.
+
+users.title=Список пользователей
+users.edit=Редактировать пользователя
+users.add=Добавить пользователя
+users.name=Имя
+users.email=Почта
+users.roles=Роли
+users.active=Активный
+users.registered=Зарегистрирован
+users.password=Пароль
+users.caloriesPerDay=Калорий в день
+
+meals.title=Список еды
+meals.edit=Редактировать еду
+meals.add=Добавить еду
+meals.filter=Отфильтровать
+meals.startDate=От даты
+meals.endDate=До даты
+meals.startTime=От времени
+meals.endTime=До времени
+meals.description=Описание
+meals.dateTime=Время
+meals.calories=Калории
+
+common.select=Выбрать
+common.delete=Удалить
+common.update=Обновить
+common.save=Сохранить
+common.search=Искать
+
+common.deleted=Удален
+common.saved=Записан
+common.enabled=Активирован
+common.disabled=Деактивирован
+common.failed=Ошибка
+common.add=Добавить
+
+exception.duplicate_email=Пользователь с такой e-почтой уже есть в приложении.
\ No newline at end of file
diff --git a/config/setenv.bat b/config/setenv.bat
new file mode 100644
index 000000000000..ebbd659386f6
--- /dev/null
+++ b/config/setenv.bat
@@ -0,0 +1,4 @@
+rem run tomcat with JMX ability
+rem Run Tomcat as admin
+rem for remote connection add -Djava.rmi.server.hostname=TomcatServer_IP
+set CATALINA_OPTS=-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
diff --git a/config/setenv.sh b/config/setenv.sh
new file mode 100644
index 000000000000..e7986cf38aae
--- /dev/null
+++ b/config/setenv.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+# run tomcat with JMX ability as admin
+# for remote connection add -Djava.rmi.server.hostname=TomcatServer_IP
+export CATALINA_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
\ No newline at end of file
diff --git a/cv.md b/cv.md
new file mode 100644
index 000000000000..42543dfdabe6
--- /dev/null
+++ b/cv.md
@@ -0,0 +1,76 @@
+## Составление резюме, подготовка к интервью, поиск работы
+
+
+
+- Научиться программировать сложнее, чем кажется
+
+### Составление резюме:
+- Выбрать шаблон для резюме
+- Идеальное резюме Junior’а
+- Как продать свое резюме в 2 раза дороже
+- Как правильно составить резюме
+- Резюме программистов. Часть 1 (плохие)
+- Резюме программистов. Часть 2 (хорошие)
+- Как составить резюме на английском
+- ОФОРМЛЕНИЕ IT-РЕЗЮМЕ для USA
+
+### Наши истории (делимся опытом и успехом)
+
+### Тесты/задачи онлайн:
+- Codility lesson tests
+- Quizful- тесты онлайн
+- Multiple-Choice Questions in Java
+- LeetCode Online Judge
+- Sphere online judge
+- Codility programmers lessons
+- Hackerrank practice coding
+
+### Интервью:
+- Михаил Портнов. Собеседование на работу: как продать себя грамотно
+- Михаил Портнов. Какие вопросы мы задаем на собеседовании?
+- Канал: Резюме, поиск работы, интервью
+- Яков Файн: Как стать профессиональным Java разработчиком
+- Список вопросов с ответами для собеседования по Java
+- Сборка по вопросам на интервью
+- Сборка вопросов-ответов от JavaStudy
+- Тест на знание SQL
+- Вопросы на собеседовании Java Junior Developer
+- Java вопросы с собеседований на Android
+- Сборка вопросов от JavaRush
+> про clone и finalize объязательно прочтите Джошуа Блох: Java. Эффективное программирование (второе издание)
+
+- Cracking the Coding Interview
+> Особенно обратите внимание на раздел: Часть VIII. Вопросы собеседования
+
+
+### От себя:
+- email, skype - очень желательно, чтобы по ним вы были узнаваемы. Заведите рабочие, если не так.
+- написать ВЕСЬ IT опыт (исключая опыт пользователя: Windows, MS Word, Photophop, Yandex disk, Google docs, ..): технологии, какие задачи решали (конкретные), какие инструменты использовали, VCS, DB, инструменты сборки, ... включая опыт в ВУЗе.
+- на English иметь желательно. Если вакансия опублинована на Englsih - шлите на нем. Часто могут на нем попросить, если работодатель иностранный.
+- удобно иметь резюме где то в инете (hh, linkedin, google doc, чтобы им было удобно делиться).
+
+### Позиционирование проекта Topjava :
+- После завершения проекта вы освоите все заявленные в нем технологии - вставьте их в квалификацию (включая java 8 Stream and Time API). В разделе опыт работы (если у вас нет коммерческого опыта) вставьте:
+
+ Участие в разработке Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей
+ на стеке Maven/ Spring MVC/ Security/ REST(Jackson)/ Java 8 Stream API:
+ - реализация сохранения в базы Postgres и HSQLDB на основе Spring JBDC, JPA(Hibernate) и Spring-Data-JPA
+ - реализация и тестирование REST и AJAX контроллеров
+ - реализация клиента на Bootstrap (css/js), datatables, jQuery + plugins.
+ - отлично будет, если вы его еще как то сами доработаете
+
+
+Делайте упор не на обучение, а на *участие в проекте*. Выполнение домашних заданий - это полноценное участие с написанием функционала по всем этим технологиям. На собеседовании меньше смотрят на то, что вы заканчивали, больше - на ваш опыт и то что вы знаете.
+
+- Ссылку на организацию можно поставить: http://javaops.ru (или в linkedin: https://www.linkedin.com/company/java-online-projects)
+
+### В течении проекта заполните форму соискателя "Разработчик Java"
+Я ее рассылаю всем знакомым HR, уже есть пинги и трудоустройство.
+
+Ближе к концу курса я вышлю приглашение в нашу общую группу slack: общение на все темы IT, обсуждение вступительных заданий, вакансии, цены на рынке труда, события, помощь, интересные видео по теме и пр.
+
+### Основные сайты поиска работы:
+- Яндекс агрегатор
+- HH
+- LinkedIn
+- djinni.co (более актуально для Украины)
diff --git a/description.md b/description.md
new file mode 100644
index 000000000000..cbb102fa72aa
--- /dev/null
+++ b/description.md
@@ -0,0 +1,67 @@
+#### Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java : Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), datatables, jQuery + plugins, Java 8 Stream and Time API
+- Основное внимание будет уделяться способам решения многочисленных проблем разработки в Spring/JPA, а также структурному (красивому и надежному) java кодированию и архитектуре приложения.
+- Каждая итерация проекта в закрепляется домашним заданием по реализации схожей функциональности. Следующее занятие начинается с разбора домашних заданий.
+- Большое внимание уделяется тестированию кода: в проекте 88 JUnit тестов.
+- Несмотря на относительно небольшой размер, приложение разрабатывается с нуля как большой проект (например мы используем кэш 2-го уровня Hibernate, настраиваем Jenkins для работы с ленивой загрузкой
+Hibernate, делаем конверторы для типов LocalDateTime (Java 8 time API), которые еще не поддерживаются ни JPA/Hibernate, ни Jackson/json).
+ Разбираются архитектурные паттерны: слои приложения и как правильно разбивать логику по слоям, когда нужно применять Data Transfer Object.
+ Т.е на выходе получается не учебный проект, а хорошо маштабируемый шаблон для большого проекта на всех пройденных технологиях.
+- Большое внимание уделяется деталям: популяция базы, использование транзакционности, тесты сервисов и REST
+ контроллеров, насторойка EntityManagerFactory,
+ выбор реализации пула коннектов. Особое внимание уделяется работе с базой: через Spring JDBC, Spring ORM и
+ Spring Data Jpa.
+- Используются самые востребованные на сегодняшний момент фреймворки: Maven, Spring Security 4
+ вместе с Spring Security Test, наиболее удобный для работы с базой проект Spring Data Jpa, библиотека логирования logback, реализующая SLF4J, повсеместно используемый Bootstrap и jQuery.
+
+#### Демо разрабатываемого приложения
+
+## План проекта (ссылки на некоторые темы открыты для просмотра)
+### Архитектура проекта. Персистентность.
+- Системы управления версиями
+- Java 8: Lambda , Stream API
+- Обзор используемых в проекте технологий и инструментов.
+- Инструмент сборки Maven.
+- WAR. Веб-контейнер Tomcat. Сервлеты.
+- Логирование.
+- Обзор стандартных библиотек. Apache Commons, Guava
+- Слои приложения. Создание каркаса приложения.
+- Обзор Spring Framework. Spring Context.
+- Тестирование через JUnit.
+- Spring Test
+- Базы данных. PostgreSQL. Обзор NoSQL и Java persistence solution без ORM.
+- Настройка Database в IDEA.
+- Скрипты инициализации базы. Spring Jdbc Template.
+- Spring: инициализация и популирование DB
+- ORM. Hibernate. JPA.
+- Поддержка HSQLDB
+- Транзакции
+- Профили Maven и Spring
+- Пул коннектов
+- Spring Data JPA
+- Кэш Hibernate
+
+### Разработка WEB
+- Spring кэш
+- Spring Web
+- JSP, JSTL, i18n
+- Tomcat maven plugin. JNDI
+- Spring Web MVC
+- Spring Internationalization
+- Тестирование Spring MVC
+- REST контроллеры
+- Тестирование REST контроллеров. Jackson.
+- jackson-datatype-hibernate. Тестирование через матчеры.
+- Тестирование через SoapUi. UTF-8
+- WebJars.
+- Bootstrap. Datatables.
+- AJAX. jQuery. Notifications.
+- Spring Security
+- Spring Binding/Validation
+- Работа с Datatables через Ajax.
+- Spring Security Test
+- Encoding password
+- CSRF (добавление в проект защиты от межсайтовой подделки запроса)
+- form-login. Spring Security Taglib
+- Handler interceptor
+- Spring Exception Handling
+- Деплой в Heroku
diff --git a/hr.bat b/hr.bat
new file mode 100644
index 000000000000..1ea30584df86
--- /dev/null
+++ b/hr.bat
@@ -0,0 +1,2 @@
+call mvn -B -s settings.xml -DskipTests=true clean package
+call java -Dspring.profiles.active="datajpa,heroku" -DDATABASE_URL="postgres://user:password@localhost:5432/topjava" -jar target/dependency/webapp-runner.jar target/*.war
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c8a1c78f3b29..24f37b9defed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
ru.javawebinar
topjava
- jar
+ war
1.0-SNAPSHOT
@@ -15,11 +15,42 @@
1.8
UTF-8
UTF-8
+
+ 8.0.33
+ 4.3.2.RELEASE
+ 4.1.3.RELEASE
+ 1.10.4.RELEASE
+ 2.8.4
+
+
+ 1.1.7
+ 1.7.21
+
+
+ 9.4.1211
+
+ 4.12
+ 1.3
+
+
+ 5.2.2.Final
+ 5.2.4.Final
+
+
+ 2.10.2.2.21
+
+
+ 3.3.7-1
+ 2.2.4
+ 2.4.7
+ 2.3.8
+ 1.10.12-1
+
topjava
- install
+ package
org.apache.maven.plugins
@@ -30,15 +61,353 @@
${java.version}
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.19.1
+
+ -Dfile.encoding=UTF-8
+
+
+
+
+
+ org.codehaus.cargo
+ cargo-maven2-plugin
+ 1.5.0
+
+
+ tomcat8x
+
+ UTF-8
+ tomcat,datajpa
+
+
+
+ org.postgresql
+ postgresql
+
+
+
+
+
+
+ src/main/resources/tomcat/context.xml
+ conf/Catalina/localhost/
+ context.xml.default
+
+
+
+
+
+ ru.javawebinar
+ topjava
+ war
+
+ ${project.build.finalName}
+
+
+
+
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+ compile
+
+
+
+ org.slf4j
+ jcl-over-slf4j
+ ${slf4j.version}
+ runtime
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ runtime
+
+
+
+
+ org.springframework
+ spring-context-support
+ ${spring.version}
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ org.springframework.data
+ spring-data-jpa
+ ${spring-data-jpa.version}
+
+
+
+ org.springframework
+ spring-webmvc
+ ${spring.version}
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+
+
+ org.springframework.security
+ spring-security-web
+ ${spring-security.version}
+
+
+ org.springframework.security
+ spring-security-config
+ ${spring-security.version}
+
+
+ org.springframework.security
+ spring-security-taglibs
+ ${spring-security.version}
+
+
+
+
+ org.hibernate
+ hibernate-core
+ ${hibernate.version}
+
+
+ javax.transaction
+ jta
+ 1.1
+ runtime
+
+
+ org.hibernate
+ hibernate-validator
+ ${hibernate-validator.version}
+
+
+ org.jsoup
+ jsoup
+ 1.10.1
+
+
+ org.hibernate
+ hibernate-ehcache
+ ${hibernate.version}
+
+
+
+ net.sf.ehcache
+ ehcache
+ ${ehcache.version}
+
+
+
+ javax.el
+ javax.el-api
+ 2.2.5
+ provided
+
+
+
+
+ org.apache.tomcat
+ tomcat-servlet-api
+ ${tomcat.version}
+ provided
+
+
+
+ javax.servlet
+ jstl
+ 1.2
+
+
+
+
+ org.webjars
+ bootstrap
+ ${webjars-bootstrap.version}
+
+
+ org.webjars
+ jquery
+ ${webjars-jquery.version}
+
+
+ org.webjars.bower
+ datetimepicker
+ ${webjars-datetimepicker.version}
+
+
+ org.webjars.bower
+ noty
+ ${webjars-noty.version}
+
+
+ org.webjars
+ datatables
+ ${webjars-datatables.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson-json.version}
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-hibernate5
+ ${jackson-json.version}
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ ${jackson-json.version}
+
+
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+ hamcrest-core
+ org.hamcrest
+
+
+
+
+ org.hamcrest
+ hamcrest-all
+ ${hamcrest.version}
+ test
+
+
+
+ org.springframework
+ spring-test
+ ${spring.version}
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ ${spring-security.version}
+
+
+ hsqldb
+
+
+ org.hsqldb
+ hsqldb
+ 2.3.4
+
+
+
+
+ postgres
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+
+
+ org.apache.tomcat
+ tomcat-jdbc
+ ${tomcat.version}
+
+
+
+ true
+
+
+
+ heroku
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 2.10
+
+
+ package
+
+ copy
+
+
+
+
+ com.github.jsimone
+ webapp-runner
+ 8.0.33.1
+ webapp-runner.jar
+
+
+
+
+
+
+
+
+
+
+
+ org.springframework
+ spring-framework-bom
+ ${spring.version}
+ pom
+ import
+
+
diff --git a/settings.xml b/settings.xml
new file mode 100644
index 000000000000..cb1c8aa69b1b
--- /dev/null
+++ b/settings.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+ postgres
+ heroku
+
+
diff --git a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java
new file mode 100644
index 000000000000..2880d109c38b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java
@@ -0,0 +1,60 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.UserUtil;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+public class AuthorizedUser extends org.springframework.security.core.userdetails.User {
+ private static final long serialVersionUID = 1L;
+
+ private UserTo userTo;
+
+ public AuthorizedUser(User user) {
+ super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles());
+ this.userTo = UserUtil.asTo(user);
+ }
+
+ public static AuthorizedUser safeGet() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null) {
+ return null;
+ }
+ Object principal = auth.getPrincipal();
+ return (principal instanceof AuthorizedUser) ? (AuthorizedUser) principal : null;
+ }
+
+ public static AuthorizedUser get() {
+ AuthorizedUser user = safeGet();
+ requireNonNull(user, "No authorized user found");
+ return user;
+ }
+
+ public static int id() {
+ return get().userTo.getId();
+ }
+
+ public static int getCaloriesPerDay() {
+ return get().userTo.getCaloriesPerDay();
+ }
+
+ public void update(UserTo newTo) {
+ userTo = newTo;
+ }
+
+ public UserTo getUserTo() {
+ return userTo;
+ }
+
+ @Override
+ public String toString() {
+ return userTo.toString();
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java
new file mode 100644
index 000000000000..c48e6fa2bcd6
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/Profiles.java
@@ -0,0 +1,18 @@
+package ru.javawebinar.topjava;
+
+/**
+ * User: gkislin
+ * Date: 19.08.2014
+ */
+public class Profiles {
+ public static final String
+ POSTGRES = "postgres",
+ HSQLDB = "hsqldb",
+ JDBC = "jdbc",
+ JPA = "jpa",
+ DATAJPA = "datajpa",
+ HEROKU = "heroku";
+
+ public static final String ACTIVE_DB = POSTGRES;
+ public static final String DB_IMPLEMENTATION = DATAJPA;
+}
diff --git a/src/main/java/ru/javawebinar/topjava/View.java b/src/main/java/ru/javawebinar/topjava/View.java
new file mode 100644
index 000000000000..b72b5ab01549
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/View.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava;
+
+/**
+ * gkislin
+ * 04.08.2016
+ */
+public class View {
+ public static class REST {}
+ public static class UI {}
+}
diff --git a/src/main/java/ru/javawebinar/topjava/model/BaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/BaseEntity.java
new file mode 100644
index 000000000000..86e5e5de2751
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/BaseEntity.java
@@ -0,0 +1,69 @@
+package ru.javawebinar.topjava.model;
+
+import org.hibernate.Hibernate;
+import org.springframework.data.domain.Persistable;
+
+import javax.persistence.*;
+
+/**
+ * User: gkislin
+ * Date: 22.08.2014
+ *
+ * Do not manipulate new (transient) entries in HashSet/HashMap without overriding hashCode
+ * http://stackoverflow.com/a/39827962/548473
+ *
+ * @see org.springframework.data.jpa.domain.AbstractPersistable
+ */
+@MappedSuperclass
+// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access
+@Access(AccessType.FIELD)
+//@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE)
+public class BaseEntity implements Persistable {
+ public static final int START_SEQ = 100000;
+
+ @Id
+ @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1)
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq")
+ // PROPERTY access for id due to bug: https://hibernate.atlassian.net/browse/HHH-3718
+ @Access(value = AccessType.PROPERTY)
+ protected Integer id;
+
+ public BaseEntity() {
+ }
+
+ protected BaseEntity(Integer id) {
+ this.id = id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ @Override
+ public Integer getId() {
+ return id;
+ }
+
+ @Override
+ public boolean isNew() {
+ return (getId() == null);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || !getClass().equals(Hibernate.getClass(o))) {
+ return false;
+ }
+ BaseEntity that = (BaseEntity) o;
+
+ return null != getId() && getId().equals(that.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return (getId() == null) ? 0 : getId();
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java
new file mode 100644
index 000000000000..4f0044144ec4
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java
@@ -0,0 +1,118 @@
+package ru.javawebinar.topjava.model;
+
+import org.hibernate.validator.constraints.NotEmpty;
+import org.hibernate.validator.constraints.Range;
+import org.hibernate.validator.constraints.SafeHtml;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+/**
+ * GKislin
+ * 11.01.2015.
+ */
+@SuppressWarnings("JpaQlInspection")
+@NamedQueries({
+ @NamedQuery(name = Meal.ALL_SORTED, query = "SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC"),
+ @NamedQuery(name = Meal.DELETE, query = "DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId"),
+ @NamedQuery(name = Meal.GET_BETWEEN, query = "SELECT m FROM Meal m " +
+ "WHERE m.user.id=:userId AND m.dateTime BETWEEN :startDate AND :endDate ORDER BY m.dateTime DESC"),
+
+// @NamedQuery(name = Meal.UPDATE, query = "UPDATE Meal m SET m.dateTime = :datetime, m.calories= :calories," +
+// "m.description=:desc where m.id=:id and m.user.id=:userId")
+})
+@Entity
+@Table(name = "meals", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meals_unique_user_datetime_idx")})
+public class Meal extends BaseEntity {
+ public static final String GET = "Meal.get";
+ public static final String ALL_SORTED = "Meal.getAll";
+ public static final String DELETE = "Meal.delete";
+ public static final String GET_BETWEEN = "Meal.getBetween";
+
+ @Column(name = "date_time", nullable = false)
+ @NotNull
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm")
+ private LocalDateTime dateTime;
+
+ @Column(name = "description", nullable = false)
+ @NotEmpty
+ @SafeHtml
+ private String description;
+
+ @Column(name = "calories", nullable = false)
+ @Range(min = 10, max = 5000)
+ @NotNull
+ protected Integer calories;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ private User user;
+
+ public Meal() {
+ }
+
+ public Meal(LocalDateTime dateTime, String description, int calories) {
+ this(null, dateTime, description, calories);
+ }
+
+ public Meal(Integer id, LocalDateTime dateTime, String description, int calories) {
+ super(id);
+ this.dateTime = dateTime;
+ this.description = description;
+ this.calories = calories;
+ }
+
+ public LocalDateTime getDateTime() {
+ return dateTime;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public LocalDate getDate() {
+ return dateTime.toLocalDate();
+ }
+
+ public LocalTime getTime() {
+ return dateTime.toLocalTime();
+ }
+
+ public void setDateTime(LocalDateTime dateTime) {
+ this.dateTime = dateTime;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public void setCalories(Integer calories) {
+ this.calories = calories;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ @Override
+ public String toString() {
+ return "Meal{" +
+ "id=" + id +
+ ", dateTime=" + dateTime +
+ ", description='" + description + '\'' +
+ ", calories=" + calories +
+ '}';
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/model/NamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/NamedEntity.java
new file mode 100644
index 000000000000..3246563c9a33
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/NamedEntity.java
@@ -0,0 +1,41 @@
+package ru.javawebinar.topjava.model;
+
+import org.hibernate.validator.constraints.NotEmpty;
+import org.hibernate.validator.constraints.SafeHtml;
+
+import javax.persistence.Column;
+import javax.persistence.MappedSuperclass;
+
+/**
+ * User: gkislin
+ * Date: 22.08.2014
+ */
+@MappedSuperclass
+public class NamedEntity extends BaseEntity {
+
+ @NotEmpty
+ @Column(name = "name", nullable = false)
+ @SafeHtml
+ protected String name;
+
+ public NamedEntity() {
+ }
+
+ protected NamedEntity(Integer id, String name) {
+ super(id);
+ this.name = name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java
new file mode 100644
index 000000000000..9c5210417f0b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/Role.java
@@ -0,0 +1,17 @@
+package ru.javawebinar.topjava.model;
+
+import org.springframework.security.core.GrantedAuthority;
+
+/**
+ * User: gkislin
+ * Date: 22.08.2014
+ */
+public enum Role implements GrantedAuthority {
+ ROLE_USER,
+ ROLE_ADMIN;
+
+ @Override
+ public String getAuthority() {
+ return name();
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java
new file mode 100644
index 000000000000..b0db4899619f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/User.java
@@ -0,0 +1,159 @@
+package ru.javawebinar.topjava.model;
+
+import com.fasterxml.jackson.annotation.JsonView;
+import org.hibernate.annotations.BatchSize;
+import org.hibernate.annotations.Cache;
+import org.hibernate.annotations.CacheConcurrencyStrategy;
+import org.hibernate.validator.constraints.Email;
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.hibernate.validator.constraints.SafeHtml;
+import org.springframework.util.CollectionUtils;
+import ru.javawebinar.topjava.View;
+import ru.javawebinar.topjava.util.UserUtil;
+
+import javax.persistence.*;
+import javax.validation.constraints.Digits;
+import java.util.*;
+
+/**
+ * User: gkislin
+ * Date: 22.08.2014
+ */
+@NamedQueries({
+ @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"),
+ @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u WHERE u.email=?1"),
+ @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u ORDER BY u.name, u.email"),
+})
+@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
+@Entity
+@NamedEntityGraph(name = User.GRAPH_WITH_MEALS, attributeNodes = {@NamedAttributeNode("meals")})
+@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "users_unique_email_idx")})
+public class User extends NamedEntity {
+
+ public static final String GRAPH_WITH_MEALS = "User.withMeals";
+
+ public static final String DELETE = "User.delete";
+ public static final String ALL_SORTED = "User.getAllSorted";
+ public static final String BY_EMAIL = "User.getByEmail";
+
+ @Column(name = "email", nullable = false, unique = true)
+ @Email
+ @NotEmpty
+ @SafeHtml
+ private String email;
+
+ @Column(name = "password", nullable = false)
+ @NotEmpty
+ @Length(min = 5)
+ @JsonView(View.REST.class)
+ @SafeHtml
+ private String password;
+
+ @Column(name = "enabled", nullable = false)
+ private boolean enabled = true;
+
+ @Column(name = "registered", columnDefinition = "timestamp default now()")
+ private Date registered = new Date();
+
+ @Enumerated(EnumType.STRING)
+ @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
+ @Column(name = "role")
+ @ElementCollection(fetch = FetchType.EAGER)
+ @BatchSize(size = 200)
+ @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
+ private Set roles;
+
+ @Column(name = "calories_per_day", columnDefinition = "int default 2000")
+ @Digits(fraction = 0, integer = 4)
+ private int caloriesPerDay = UserUtil.DEFAULT_CALORIES_PER_DAY;
+
+ @OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, mappedBy = "user")
+ @OrderBy("dateTime DESC")
+// @JsonIgnore
+ protected List meals;
+
+ public User() {
+ }
+
+ public User(User u) {
+ this(u.getId(), u.getName(), u.getEmail(), u.getPassword(), u.getCaloriesPerDay(), u.isEnabled(), u.getRoles());
+ }
+
+ public User(Integer id, String name, String email, String password, int caloriesPerDay, Role role, Role... roles) {
+ this(id, name, email, password, caloriesPerDay, true, EnumSet.of(role, roles));
+ }
+
+ public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Set roles) {
+ super(id, name);
+ this.email = email;
+ this.password = password;
+ this.caloriesPerDay = caloriesPerDay;
+ this.enabled = enabled;
+ setRoles(roles);
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public Date getRegistered() {
+ return registered;
+ }
+
+ public void setRegistered(Date registered) {
+ this.registered = registered;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public int getCaloriesPerDay() {
+ return caloriesPerDay;
+ }
+
+ public void setCaloriesPerDay(int caloriesPerDay) {
+ this.caloriesPerDay = caloriesPerDay;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public Set getRoles() {
+ return roles;
+ }
+
+ public void setRoles(Collection roles) {
+ this.roles = CollectionUtils.isEmpty(roles) ? Collections.emptySet() : EnumSet.copyOf(roles);
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public List getMeals() {
+ return meals;
+ }
+
+ @Override
+ public String toString() {
+ return "User (" +
+ "id=" + id +
+ ", email=" + email +
+ ", name=" + name +
+ ", enabled=" + enabled +
+ ", roles=" + roles +
+ ", caloriesPerDay=" + caloriesPerDay +
+ ')';
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java
new file mode 100644
index 000000000000..75b3d82ba3cb
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java
@@ -0,0 +1,25 @@
+package ru.javawebinar.topjava.repository;
+
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+
+public class JpaUtil {
+
+ @PersistenceContext
+ private EntityManager em;
+
+ public void clear2ndLevelHibernateCache() {
+ Session s = (Session) em.getDelegate();
+ SessionFactory sf = s.getSessionFactory();
+// sf.evict(User.class);
+// sf.getCache().evictEntity(User.class, BaseEntity.START_SEQ);
+// sf.getCache().evictEntityRegion(User.class);
+ sf.getCache().evictQueryRegions();
+ sf.getCache().evictDefaultQueryRegion();
+ sf.getCache().evictCollectionRegions();
+ sf.getCache().evictEntityRegions();
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java
new file mode 100644
index 000000000000..1645a2f9ae49
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java
@@ -0,0 +1,31 @@
+package ru.javawebinar.topjava.repository;
+
+import ru.javawebinar.topjava.model.Meal;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+public interface MealRepository {
+ // null if updated meal do not belong to userId
+ Meal save(Meal meal, int userId);
+
+ // false if meal do not belong to userId
+ boolean delete(int id, int userId);
+
+ // null if meal do not belong to userId
+ Meal get(int id, int userId);
+
+ // ORDERED dateTime
+ Collection getAll(int userId);
+
+ // ORDERED dateTime
+ Collection getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId);
+
+ default Meal getWithUser(int id, int userId) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java
new file mode 100644
index 000000000000..47ded4cec6b8
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java
@@ -0,0 +1,28 @@
+package ru.javawebinar.topjava.repository;
+
+import ru.javawebinar.topjava.model.User;
+
+import java.util.List;
+
+/**
+ * User: gkislin
+ * Date: 22.08.2014
+ */
+public interface UserRepository {
+ User save(User user);
+
+ // false if not found
+ boolean delete(int id);
+
+ // null if not found
+ User get(int id);
+
+ // null if not found
+ User getByEmail(String email);
+
+ List getAll();
+
+ default User getWithMeals(int id){
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java
new file mode 100644
index 000000000000..530a81086f0a
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava.repository.datajpa;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.model.Meal;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * gkislin
+ * 02.10.2016
+ */
+@Transactional(readOnly = true)
+public interface CrudMealRepository extends JpaRepository {
+
+ @Modifying
+ @Transactional
+ @Query("DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId")
+ int delete(@Param("id") int id, @Param("userId") int userId);
+
+ @Override
+ Meal save(Meal item);
+
+ @Query("SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC")
+ List getAll(@Param("userId") int userId);
+
+ @SuppressWarnings("JpaQlInspection")
+ @Query("SELECT m from Meal m WHERE m.user.id=:userId AND m.dateTime BETWEEN :startDate AND :endDate ORDER BY m.dateTime DESC")
+ List getBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("userId") int userId);
+
+ @Query("SELECT m FROM Meal m JOIN FETCH m.user WHERE m.id = ?1 and m.user.id = ?2")
+ Meal getWithUser(int id, int userId);
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java
new file mode 100644
index 000000000000..d310c388cbc7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java
@@ -0,0 +1,41 @@
+package ru.javawebinar.topjava.repository.datajpa;
+
+import org.springframework.data.jpa.repository.EntityGraph;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.model.User;
+
+import java.util.List;
+
+/**
+ * gkislin
+ * 02.10.2016
+ */
+@Transactional(readOnly = true)
+public interface CrudUserRepository extends JpaRepository {
+ @Transactional
+ @Modifying
+ @Query("DELETE FROM User u WHERE u.id=:id")
+ int delete(@Param("id") int id);
+
+ @Override
+ @Transactional
+ User save(User user);
+
+ @Override
+ User findOne(Integer id);
+
+ @Override
+ @Query("SELECT u FROM User u ORDER BY u.name, u.email")
+ List findAll();
+
+ @Query("SELECT u FROM User u WHERE u.email=?1")
+ User getByEmail(String email);
+
+ @EntityGraph(value = User.GRAPH_WITH_MEALS)
+ @Query("SELECT u FROM User u WHERE u.id=?1")
+ User getWithMeals(int id);
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepositoryImpl.java
new file mode 100644
index 000000000000..d3a6c2c1bd56
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepositoryImpl.java
@@ -0,0 +1,60 @@
+package ru.javawebinar.topjava.repository.datajpa;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * GKislin
+ * 27.03.2015.
+ */
+@Repository
+public class DataJpaMealRepositoryImpl implements MealRepository {
+
+ @Autowired
+ private CrudMealRepository crudRepository;
+
+ @Autowired
+ private CrudUserRepository crudUserRepository;
+
+ @Override
+ @Transactional
+ public Meal save(Meal meal, int userId) {
+ if (!meal.isNew() && get(meal.getId(), userId) == null) {
+ return null;
+ }
+ meal.setUser(crudUserRepository.getOne(userId));
+ return crudRepository.save(meal);
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ return crudRepository.delete(id, userId) != 0;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ Meal meal = crudRepository.findOne(id);
+ return meal != null && meal.getUser().getId() == userId ? meal : null;
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return crudRepository.getAll(userId);
+ }
+
+ @Override
+ public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) {
+ return crudRepository.getBetween(startDate, endDate, userId);
+ }
+
+ @Override
+ public Meal getWithUser(int id, int userId) {
+ return crudRepository.getWithUser(id, userId);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java
new file mode 100644
index 000000000000..18e127fed47a
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java
@@ -0,0 +1,50 @@
+package ru.javawebinar.topjava.repository.datajpa;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import java.util.List;
+
+/**
+ * GKislin
+ * 27.03.2015.
+ */
+
+@Repository
+public class DataJpaUserRepositoryImpl implements UserRepository {
+
+ @Autowired
+ private CrudUserRepository crudRepository;
+
+ @Override
+ public User save(User user) {
+ return crudRepository.save(user);
+ }
+
+ @Override
+ public boolean delete(int id) {
+ return crudRepository.delete(id) != 0;
+ }
+
+ @Override
+ public User get(int id) {
+ return crudRepository.findOne(id);
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ return crudRepository.getByEmail(email);
+ }
+
+ @Override
+ public List getAll() {
+ return crudRepository.findAll();
+ }
+
+ @Override
+ public User getWithMeals(int id) {
+ return crudRepository.getWithMeals(id);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepositoryImpl.java
new file mode 100644
index 000000000000..ddb69c0df063
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepositoryImpl.java
@@ -0,0 +1,119 @@
+package ru.javawebinar.topjava.repository.jdbc;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.dao.support.DataAccessUtils;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.Profiles;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+
+import javax.sql.DataSource;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * User: gkislin
+ * Date: 26.08.2014
+ */
+
+@Transactional(readOnly = true)
+public abstract class JdbcMealRepositoryImpl implements MealRepository {
+
+ private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class);
+
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+
+ private SimpleJdbcInsert insertMeal;
+
+ protected abstract T toDbDateTime(LocalDateTime ldt);
+
+ @Autowired
+ private void setDataSource(DataSource dataSource) {
+ this.insertMeal = new SimpleJdbcInsert(dataSource)
+ .withTableName("meals")
+ .usingGeneratedKeyColumns("id");
+ }
+
+ @Repository
+ @Profile(Profiles.POSTGRES)
+ public static class Java8JdbcMealRepositoryImpl extends JdbcMealRepositoryImpl {
+ @Override
+ protected LocalDateTime toDbDateTime(LocalDateTime ldt) {
+ return ldt;
+ }
+ }
+
+ @Repository
+ @Profile(Profiles.HSQLDB)
+ public static class TimestampJdbcMealRepositoryImpl extends JdbcMealRepositoryImpl {
+
+ @Override
+ protected Timestamp toDbDateTime(LocalDateTime ldt) {
+ return Timestamp.valueOf(ldt);
+ }
+ }
+
+ @Override
+ @Transactional
+ public Meal save(Meal meal, int userId) {
+ MapSqlParameterSource map = new MapSqlParameterSource()
+ .addValue("id", meal.getId())
+ .addValue("description", meal.getDescription())
+ .addValue("calories", meal.getCalories())
+ .addValue("date_time", toDbDateTime(meal.getDateTime()))
+ .addValue("user_id", userId);
+
+ if (meal.isNew()) {
+ Number newId = insertMeal.executeAndReturnKey(map);
+ meal.setId(newId.intValue());
+ } else {
+ if (namedParameterJdbcTemplate.update("" +
+ "UPDATE meals " +
+ " SET description=:description, calories=:calories, date_time=:date_time " +
+ " WHERE id=:id AND user_id=:user_id"
+ , map) == 0) {
+ return null;
+ }
+ }
+ return meal;
+ }
+
+ @Override
+ @Transactional
+ public boolean delete(int id, int userId) {
+ return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ List meals = jdbcTemplate.query(
+ "SELECT * FROM meals WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId);
+ return DataAccessUtils.singleResult(meals);
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return jdbcTemplate.query(
+ "SELECT * FROM meals WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId);
+ }
+
+ @Override
+ public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) {
+ return jdbcTemplate.query(
+ "SELECT * FROM meals WHERE user_id=? AND date_time BETWEEN ? AND ? ORDER BY date_time DESC",
+ ROW_MAPPER, userId, toDbDateTime(startDate), toDbDateTime(endDate));
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java
new file mode 100644
index 000000000000..300574997076
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java
@@ -0,0 +1,160 @@
+package ru.javawebinar.topjava.repository.jdbc;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.support.DataAccessUtils;
+import org.springframework.jdbc.core.BatchPreparedStatementSetter;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * User: gkislin
+ * Date: 26.08.2014
+ */
+
+@Repository
+@Transactional(readOnly = true)
+public class JdbcUserRepositoryImpl implements UserRepository {
+
+ private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class);
+ private static final RowMapper ROLE_ROW_MAPPER = (rs, rowNum) -> Role.valueOf(rs.getString("role"));
+
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+
+ private SimpleJdbcInsert insertUser;
+
+ @Autowired
+ public JdbcUserRepositoryImpl(DataSource dataSource) {
+ this.insertUser = new SimpleJdbcInsert(dataSource)
+ .withTableName("USERS")
+ .usingGeneratedKeyColumns("id");
+ }
+
+ @Override
+ @Transactional
+ public User save(User user) {
+ MapSqlParameterSource map = new MapSqlParameterSource()
+ .addValue("id", user.getId())
+ .addValue("name", user.getName())
+ .addValue("email", user.getEmail())
+ .addValue("password", user.getPassword())
+ .addValue("registered", user.getRegistered())
+ .addValue("enabled", user.isEnabled())
+ .addValue("caloriesPerDay", user.getCaloriesPerDay());
+
+ if (user.isNew()) {
+ Number newKey = insertUser.executeAndReturnKey(map);
+ user.setId(newKey.intValue());
+ insertRoles(user);
+ } else {
+ deleteRoles(user);
+ insertRoles(user);
+ namedParameterJdbcTemplate.update(
+ "UPDATE users SET name=:name, email=:email, password=:password, " +
+ "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", map);
+ }
+ return user;
+ }
+
+ @Override
+ @Transactional
+ public boolean delete(int id) {
+ return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0;
+ }
+
+ @Override
+ public User get(int id) {
+ List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id);
+ return setRoles(DataAccessUtils.singleResult(users));
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email);
+ return setRoles(DataAccessUtils.singleResult(users));
+ }
+
+ @Override
+ public List getAll() {
+ class UserRole {
+ final private int userId;
+ final private Role role;
+
+ private UserRole(int userId, Role role) {
+ this.userId = userId;
+ this.role = role;
+ }
+
+ public int getUserId() {
+ return userId;
+ }
+
+ public Role getRole() {
+ return role;
+ }
+ }
+
+ Map> userRolesMap = jdbcTemplate.query("SELECT role, user_id FROM user_roles",
+ (rs, rowNum) -> new UserRole(rs.getInt("user_id"), Role.valueOf(rs.getString("role")))).stream()
+ .collect(
+ Collectors.groupingBy(UserRole::getUserId, Collectors.mapping(UserRole::getRole, Collectors.toList()))
+ );
+
+ List users = jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER);
+ users.forEach(u -> u.setRoles(userRolesMap.get(u.getId())));
+ return users;
+ }
+
+ private void insertRoles(User u) {
+ Set roles = u.getRoles();
+ Iterator iterator = roles.iterator();
+
+ jdbcTemplate.batchUpdate("INSERT INTO user_roles (user_id, role) VALUES (?, ?)",
+ new BatchPreparedStatementSetter() {
+ @Override
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ ps.setInt(1, u.getId());
+ ps.setString(2, iterator.next().name());
+ }
+
+ @Override
+ public int getBatchSize() {
+ return roles.size();
+ }
+ });
+ }
+
+ private void deleteRoles(User u) {
+ jdbcTemplate.update("DELETE FROM user_roles WHERE user_id=?", u.getId());
+ }
+
+ private User setRoles(User u) {
+ if (u != null) {
+ List roles = jdbcTemplate.query("SELECT role FROM user_roles WHERE user_id=?",
+ ROLE_ROW_MAPPER, u.getId());
+ u.setRoles(roles);
+ }
+ return u;
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepositoryImpl.java
new file mode 100644
index 000000000000..200a2c018745
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepositoryImpl.java
@@ -0,0 +1,70 @@
+package ru.javawebinar.topjava.repository.jpa;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.MealRepository;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * User: gkisline
+ * Date: 26.08.2014
+ */
+
+@Repository
+@Transactional(readOnly = true)
+public class JpaMealRepositoryImpl implements MealRepository {
+
+ @PersistenceContext
+ private EntityManager em;
+
+ @Override
+ @Transactional
+ public Meal save(Meal meal, int userId) {
+ if (!meal.isNew() && get(meal.getId(), userId) == null) {
+ return null;
+ }
+ meal.setUser(em.getReference(User.class, userId));
+ if (meal.isNew()) {
+ em.persist(meal);
+ return meal;
+ } else {
+ return em.merge(meal);
+ }
+ }
+
+ @Override
+ @Transactional
+ public boolean delete(int id, int userId) {
+ return em.createNamedQuery(Meal.DELETE)
+ .setParameter("id", id)
+ .setParameter("userId", userId)
+ .executeUpdate() != 0;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ Meal meal = em.find(Meal.class, id);
+ return meal != null && meal.getUser().getId() == userId ? meal : null;
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return em.createNamedQuery(Meal.ALL_SORTED, Meal.class)
+ .setParameter("userId", userId)
+ .getResultList();
+ }
+
+ @Override
+ public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) {
+ return em.createNamedQuery(Meal.GET_BETWEEN, Meal.class)
+ .setParameter("userId", userId)
+ .setParameter("startDate", startDate)
+ .setParameter("endDate", endDate).getResultList();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java
new file mode 100644
index 000000000000..30a5fc452a58
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java
@@ -0,0 +1,72 @@
+package ru.javawebinar.topjava.repository.jpa;
+
+import org.springframework.dao.support.DataAccessUtils;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import java.util.List;
+
+/**
+ * User: gkislin
+ * Date: 29.08.2014
+ */
+@Repository
+@Transactional(readOnly = true)
+public class JpaUserRepositoryImpl implements UserRepository {
+
+/*
+ @Autowired
+ private SessionFactory sessionFactory;
+
+ private Session openSession() {
+ return sessionFactory.getCurrentSession();
+ }
+*/
+
+ @PersistenceContext
+ private EntityManager em;
+
+ @Override
+ @Transactional
+ public User save(User user) {
+ if (user.isNew()) {
+ em.persist(user);
+ return user;
+ } else {
+ return em.merge(user);
+ }
+ }
+
+ @Override
+ public User get(int id) {
+ return em.find(User.class, id);
+ }
+
+ @Override
+ @Transactional
+ public boolean delete(int id) {
+
+/* User ref = em.getReference(User.class, id);
+ em.remove(ref);
+
+ Query query = em.createQuery("DELETE FROM User u WHERE u.id=:id");
+ return query.setParameter("id", id).executeUpdate() != 0;
+*/
+ return em.createNamedQuery(User.DELETE).setParameter("id", id).executeUpdate() != 0;
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ List users = em.createNamedQuery(User.BY_EMAIL, User.class).setParameter(1, email).getResultList();
+ return DataAccessUtils.singleResult(users);
+ }
+
+ @Override
+ public List getAll() {
+ return em.createNamedQuery(User.ALL_SORTED, User.class).getResultList();
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java
new file mode 100644
index 000000000000..1dbdd716033d
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java
@@ -0,0 +1,33 @@
+package ru.javawebinar.topjava.service;
+
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Collection;
+
+/**
+ * GKislin
+ * 15.06.2015.
+ */
+public interface MealService {
+ Meal get(int id, int userId) throws NotFoundException;
+
+ void delete(int id, int userId) throws NotFoundException;
+
+ default Collection getBetweenDates(LocalDate startDate, LocalDate endDate, int userId) {
+ return getBetweenDateTimes(LocalDateTime.of(startDate, LocalTime.MIN), LocalDateTime.of(endDate, LocalTime.MAX), userId);
+ }
+
+ Collection getBetweenDateTimes(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId);
+
+ Collection getAll(int userId);
+
+ Meal update(Meal meal, int userId) throws NotFoundException;
+
+ Meal save(Meal meal, int userId);
+
+ Meal getWithUser(int id, int userId);
+}
diff --git a/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java
new file mode 100644
index 000000000000..97a465234702
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java
@@ -0,0 +1,68 @@
+package ru.javawebinar.topjava.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+import ru.javawebinar.topjava.util.exception.ExceptionUtil;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+@Service
+public class MealServiceImpl implements MealService {
+
+ @Autowired
+ private MealRepository repository;
+
+ @Override
+ public Meal get(int id, int userId) {
+ return ExceptionUtil.checkNotFoundWithId(repository.get(id, userId), id);
+ }
+
+ @Override
+ public void delete(int id, int userId) {
+ ExceptionUtil.checkNotFoundWithId(repository.delete(id, userId), id);
+ }
+
+ @Override
+ public Collection getBetweenDateTimes(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ Assert.notNull(startDateTime, "startDateTime must not be null");
+ Assert.notNull(endDateTime, "endDateTime must not be null");
+ return repository.getBetween(startDateTime, endDateTime, userId);
+ }
+
+ @Override
+ public Collection getAll(int userId) {
+ return repository.getAll(userId);
+ }
+
+ @Override
+ public Meal update(Meal meal, int userId) {
+ Assert.notNull(meal, "meal must not be null");
+ return ExceptionUtil.checkNotFoundWithId(repository.save(meal, userId), meal.getId());
+ }
+
+ @Override
+ public Meal save(Meal meal, int userId) {
+ Assert.notNull(meal, "meal must not be null");
+ return repository.save(meal, userId);
+ }
+
+// @Autowired
+// private UserRepository userRepository;
+
+// @Transactional
+ public Meal getWithUser(int id, int userId) {
+// Meal meal = get(id, userId);
+// meal.setUser(userRepository.get(userId));
+// return meal;
+ return ExceptionUtil.checkNotFoundWithId(repository.getWithUser(id, userId), id);
+ }
+
+}
diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java
new file mode 100644
index 000000000000..ac1b88529775
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java
@@ -0,0 +1,35 @@
+package ru.javawebinar.topjava.service;
+
+
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.util.List;
+
+/**
+ * User: gkislin
+ * Date: 22.08.2014
+ */
+public interface UserService {
+
+ User save(User user);
+
+ void delete(int id) throws NotFoundException;
+
+ User get(int id) throws NotFoundException;
+
+ User getByEmail(String email) throws NotFoundException;
+
+ void update(UserTo user);
+
+ List getAll();
+
+ void update(User user);
+
+ void evictCache();
+
+ void enable(int id, boolean enable);
+
+ User getWithMeals(int id);
+}
diff --git a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java
new file mode 100644
index 000000000000..50f1750e6f5e
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java
@@ -0,0 +1,106 @@
+package ru.javawebinar.topjava.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.exception.ExceptionUtil;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.UserUtil.prepareToSave;
+import static ru.javawebinar.topjava.util.UserUtil.updateFromTo;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+@Service("userService")
+public class UserServiceImpl implements UserService, UserDetailsService {
+
+ @Autowired
+ private UserRepository repository;
+
+ @CacheEvict(value = "users", allEntries = true)
+ @Override
+ public User save(User user) {
+ Assert.notNull(user, "user must not be null");
+ return repository.save(prepareToSave(user));
+ }
+
+ @CacheEvict(value = "users", allEntries = true)
+ @Override
+ public void delete(int id) {
+ ExceptionUtil.checkNotFoundWithId(repository.delete(id), id);
+ }
+
+ @Override
+ public User get(int id) throws NotFoundException {
+ return ExceptionUtil.checkNotFoundWithId(repository.get(id), id);
+ }
+
+ @Override
+ public User getByEmail(String email) throws NotFoundException {
+ Assert.notNull(email, "email must not be null");
+ return ExceptionUtil.checkNotFound(repository.getByEmail(email), "email=" + email);
+ }
+
+ @Cacheable("users")
+ @Override
+ public List getAll() {
+ return repository.getAll();
+ }
+
+ @CacheEvict(value = "users", allEntries = true)
+ @Override
+ public void update(User user) {
+ Assert.notNull(user, "user must not be null");
+ repository.save(prepareToSave(user));
+ }
+
+ @CacheEvict(value = "users", allEntries = true)
+ @Transactional
+ @Override
+ public void update(UserTo userTo) {
+ User user = updateFromTo(get(userTo.getId()), userTo);
+ repository.save(prepareToSave(user));
+ }
+
+
+ @CacheEvict(value = "users", allEntries = true)
+ @Override
+ public void evictCache() {
+ }
+
+ @CacheEvict(value = "users", allEntries = true)
+ @Override
+ @Transactional
+ public void enable(int id, boolean enabled) {
+ User user = get(id);
+ user.setEnabled(enabled);
+ repository.save(user);
+ }
+
+ @Override
+ public AuthorizedUser loadUserByUsername(String email) throws UsernameNotFoundException {
+ User u = repository.getByEmail(email.toLowerCase());
+ if (u == null) {
+ throw new UsernameNotFoundException("User " + email + " is not found");
+ }
+ return new AuthorizedUser(u);
+ }
+
+ @Override
+ public User getWithMeals(int id) {
+ return ExceptionUtil.checkNotFoundWithId(repository.getWithMeals(id), id);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/to/MealWithExceed.java b/src/main/java/ru/javawebinar/topjava/to/MealWithExceed.java
new file mode 100644
index 000000000000..f94ba51fe7cd
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/to/MealWithExceed.java
@@ -0,0 +1,64 @@
+package ru.javawebinar.topjava.to;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.time.LocalDateTime;
+
+/**
+ * GKislin
+ * 11.01.2015.
+ */
+public class MealWithExceed {
+ private final Integer id;
+
+ private final LocalDateTime dateTime;
+
+ private final String description;
+
+ private final int calories;
+
+ private final boolean exceed;
+
+ public MealWithExceed(@JsonProperty("id") Integer id,
+ @JsonProperty("dateTime") LocalDateTime dateTime,
+ @JsonProperty("description") String description,
+ @JsonProperty("calories") int calories,
+ @JsonProperty("exceed") boolean exceed) {
+ this.id = id;
+ this.dateTime = dateTime;
+ this.description = description;
+ this.calories = calories;
+ this.exceed = exceed;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public LocalDateTime getDateTime() {
+ return dateTime;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public boolean isExceed() {
+ return exceed;
+ }
+
+ @Override
+ public String toString() {
+ return "MealWithExceed{" +
+ "id=" + id +
+ ", dateTime=" + dateTime +
+ ", description='" + description + '\'' +
+ ", calories=" + calories +
+ ", exceed=" + exceed +
+ '}';
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java
new file mode 100644
index 000000000000..e668a987ce73
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java
@@ -0,0 +1,99 @@
+package ru.javawebinar.topjava.to;
+
+import org.hibernate.validator.constraints.Email;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.hibernate.validator.constraints.Range;
+import org.hibernate.validator.constraints.SafeHtml;
+import ru.javawebinar.topjava.util.UserUtil;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.io.Serializable;
+
+public class UserTo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private Integer id;
+
+ @NotEmpty
+ @SafeHtml
+ private String name;
+
+ @Email
+ @NotEmpty
+ @SafeHtml
+ private String email;
+
+ @Size(min = 5, max = 64, message = " must between 5 and 64 characters")
+ @SafeHtml
+ private String password;
+
+ @Range(min = 100, max = 5000)
+ @NotNull(message = " must not be empty")
+ private Integer caloriesPerDay = UserUtil.DEFAULT_CALORIES_PER_DAY;
+
+ public UserTo() {
+ }
+
+ public UserTo(Integer id, String name, String email, String password, int caloriesPerDay) {
+ this.id = id;
+ this.name = name;
+ this.email = email;
+ this.password = password;
+ this.caloriesPerDay = caloriesPerDay;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public boolean isNew() {
+ return id == null;
+ }
+
+ public Integer getCaloriesPerDay() {
+ return caloriesPerDay;
+ }
+
+ public void setCaloriesPerDay(Integer caloriesPerDay) {
+ this.caloriesPerDay = caloriesPerDay;
+ }
+
+ @Override
+ public String toString() {
+ return "UserTo{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ ", email='" + email + '\'' +
+ ", caloriesPerDay='" + caloriesPerDay + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java
new file mode 100644
index 000000000000..0ec65f64652b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java
@@ -0,0 +1,51 @@
+package ru.javawebinar.topjava.util;
+
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealWithExceed;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * GKislin
+ * 31.05.2015.
+ */
+public class MealsUtil {
+
+ public static List getWithExceeded(Collection meals, int caloriesPerDay) {
+ return getFilteredWithExceeded(meals, LocalTime.MIN, LocalTime.MAX, caloriesPerDay);
+ }
+
+ public static List getFilteredWithExceeded(Collection meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ Map caloriesSumByDate = meals.stream()
+ .collect(
+ Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories))
+// Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum)
+ );
+
+ return meals.stream()
+ .filter(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime))
+ .map(meal -> createWithExceed(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay))
+ .collect(Collectors.toList());
+ }
+
+ public static List getFilteredWithExceededByCycle(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+
+ final Map caloriesSumByDate = new HashMap<>();
+ meals.forEach(meal -> caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum));
+
+ final List mealExceeded = new ArrayList<>();
+ meals.forEach(meal -> {
+ if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) {
+ mealExceeded.add(createWithExceed(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay));
+ }
+ });
+ return mealExceeded;
+ }
+
+ public static MealWithExceed createWithExceed(Meal meal, boolean exceeded) {
+ return new MealWithExceed(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), exceeded);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/util/PasswordUtil.java b/src/main/java/ru/javawebinar/topjava/util/PasswordUtil.java
new file mode 100644
index 000000000000..07faed1e87d7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/PasswordUtil.java
@@ -0,0 +1,39 @@
+package ru.javawebinar.topjava.util;
+
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.util.StringUtils;
+
+import java.util.regex.Pattern;
+
+/**
+ * GKislin
+ * 05.01.2015.
+ */
+
+public class PasswordUtil {
+ private static final Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
+ public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
+
+ public static PasswordEncoder getPasswordEncoder() {
+ return PASSWORD_ENCODER;
+ }
+
+ public static String encode(String newPassword) {
+ if (StringUtils.isEmpty(newPassword)) {
+ return null;
+ }
+ if (isEncoded(newPassword)) {
+ return newPassword;
+ }
+ return PASSWORD_ENCODER.encode(newPassword);
+ }
+
+ public static boolean isMatch(String rawPassword, String password) {
+ return PASSWORD_ENCODER.matches(rawPassword, password);
+ }
+
+ public static boolean isEncoded(String newPassword) {
+ return BCRYPT_PATTERN.matcher(newPassword).matches();
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java
new file mode 100644
index 000000000000..1d861de93c2e
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java
@@ -0,0 +1,46 @@
+package ru.javawebinar.topjava.util;
+
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * GKislin
+ * 07.01.2015.
+ */
+public class TimeUtil {
+ public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+
+ public static final LocalDate MIN_DATE = LocalDate.of(1, 1, 1);
+ public static final LocalDate MAX_DATE = LocalDate.of(3000, 1, 1);
+
+ private TimeUtil() {
+ }
+
+ public static > boolean isBetween(T value, T start, T end) {
+ return value.compareTo(start) >= 0 && value.compareTo(end) <= 0;
+ }
+
+ public static String toString(LocalDateTime ldt) {
+ return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER);
+ }
+
+ public static LocalDate parseLocalDate(String str) {
+ return StringUtils.isEmpty(str) ? null : LocalDate.parse(str);
+ }
+
+ public static LocalTime parseLocalTime(String str) {
+ return StringUtils.isEmpty(str) ? null : LocalTime.parse(str);
+ }
+
+ public static LocalDateTime parseLocalDateTime(String str) {
+ return parseLocalDateTime(str, DATE_TIME_FORMATTER);
+ }
+
+ public static LocalDateTime parseLocalDateTime(String str, DateTimeFormatter formatter) {
+ return StringUtils.isEmpty(str) ? LocalDateTime.now() : LocalDateTime.parse(str, formatter);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/util/UserUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java
new file mode 100644
index 000000000000..b005160aeff7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java
@@ -0,0 +1,35 @@
+package ru.javawebinar.topjava.util;
+
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+
+/**
+ * GKislin
+ */
+public class UserUtil {
+
+ public static final int DEFAULT_CALORIES_PER_DAY = 2000;
+
+ public static User createNewFromTo(UserTo newUser) {
+ return new User(null, newUser.getName(), newUser.getEmail().toLowerCase(), newUser.getPassword(), newUser.getCaloriesPerDay(), Role.ROLE_USER);
+ }
+
+ public static UserTo asTo(User user) {
+ return new UserTo(user.getId(), user.getName(), user.getEmail(), user.getPassword(), user.getCaloriesPerDay());
+ }
+
+ public static User updateFromTo(User user, UserTo userTo) {
+ user.setName(userTo.getName());
+ user.setEmail(userTo.getEmail());
+ user.setCaloriesPerDay(userTo.getCaloriesPerDay());
+ user.setPassword(userTo.getPassword());
+ return user;
+ }
+
+ public static User prepareToSave(User user) {
+ user.setPassword(PasswordUtil.encode(user.getPassword()));
+ user.setEmail(user.getEmail().toLowerCase());
+ return user;
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/util/converter/DateTimeFormatters.java
new file mode 100644
index 000000000000..e152d505c414
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/converter/DateTimeFormatters.java
@@ -0,0 +1,42 @@
+package ru.javawebinar.topjava.util.converter;
+
+import org.springframework.format.Formatter;
+import ru.javawebinar.topjava.util.TimeUtil;
+
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+/**
+ * gkislin
+ * 25.10.2016
+ */
+public class DateTimeFormatters {
+ public static class LocalDateFormatter implements Formatter {
+
+ @Override
+ public LocalDate parse(String text, Locale locale) throws ParseException {
+ return TimeUtil.parseLocalDate(text);
+ }
+
+ @Override
+ public String print(LocalDate lt, Locale locale) {
+ return lt.format(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+ }
+
+ public static class LocalTimeFormatter implements Formatter {
+
+ @Override
+ public LocalTime parse(String text, Locale locale) throws ParseException {
+ return TimeUtil.parseLocalTime(text);
+ }
+
+ @Override
+ public String print(LocalTime lt, Locale locale) {
+ return lt.format(DateTimeFormatter.ISO_LOCAL_TIME);
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java
new file mode 100644
index 000000000000..8f8eb49cf036
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java
@@ -0,0 +1,21 @@
+package ru.javawebinar.topjava.util.exception;
+
+/**
+ * User: gkislin
+ * Date: 19.08.2014
+ */
+public class ErrorInfo {
+ private final String url;
+ private final String cause;
+ private final String[] details;
+
+ public ErrorInfo(CharSequence url, Throwable ex) {
+ this(url, ex.getClass().getSimpleName(), ex.getLocalizedMessage());
+ }
+
+ public ErrorInfo(CharSequence requestURL, String cause, String... details) {
+ this.url = requestURL.toString();
+ this.cause = cause;
+ this.details = details;
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ExceptionUtil.java b/src/main/java/ru/javawebinar/topjava/util/exception/ExceptionUtil.java
new file mode 100644
index 000000000000..db860486466f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/exception/ExceptionUtil.java
@@ -0,0 +1,30 @@
+package ru.javawebinar.topjava.util.exception;
+
+
+/**
+ * User: gkislin
+ * Date: 14.05.2014
+ */
+public class ExceptionUtil {
+ private ExceptionUtil() {
+ }
+
+ public static void checkNotFoundWithId(boolean found, int id) {
+ checkNotFound(found, "id=" + id);
+ }
+
+ public static T checkNotFoundWithId(T object, int id) {
+ return checkNotFound(object, "id=" + id);
+ }
+
+ public static 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..7a770f0db834
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
@@ -0,0 +1,11 @@
+package ru.javawebinar.topjava.util.exception;
+
+/**
+ * User: gkislin
+ * Date: 19.08.2014
+ */
+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..6d664292d769
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java
@@ -0,0 +1,84 @@
+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);
+
+// http://stackoverflow.com/a/22358422/548473
+ @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY)
+ @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);
+ }
+
+ public 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..df6a5d25c1f6
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java
@@ -0,0 +1,92 @@
+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.*;
+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 {
+
+ @GetMapping("/")
+ public String root() {
+ return "redirect:meals";
+ }
+
+ // @Secured("ROLE_ADMIN")
+ @PreAuthorize("hasRole('ROLE_ADMIN')")
+ @GetMapping("/users")
+ public String users() {
+ return "users";
+ }
+
+ @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";
+ }
+
+ @GetMapping("/meals")
+ public String meals() {
+ return "meals";
+ }
+
+ @GetMapping("/profile")
+ public String profile() {
+ return "profile";
+ }
+
+ @PostMapping("/profile")
+ 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";
+ }
+
+ @GetMapping("/register")
+ public String register(ModelMap model) {
+ model.addAttribute("userTo", new UserTo());
+ model.addAttribute("register", true);
+ return "profile";
+ }
+
+ @PostMapping("/register")
+ public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) {
+ if (!result.hasErrors()) {
+ 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..cdd7fcdd4d3b
--- /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();
+
+ public static ObjectMapper getMapper() {
+ return MAPPER;
+ }
+
+ private JacksonObjectMapper() {
+ registerModule(new Hibernate5Module());
+
+ registerModule(new JavaTimeModule());
+ configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+
+ setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
+ setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
new file mode 100644
index 000000000000..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/AbstractMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
new file mode 100644
index 000000000000..8d8d4d0f2edf
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.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.Meal;
+import ru.javawebinar.topjava.service.MealService;
+import ru.javawebinar.topjava.to.MealWithExceed;
+import ru.javawebinar.topjava.util.MealsUtil;
+import ru.javawebinar.topjava.util.TimeUtil;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+/**
+ * GKislin
+ * 06.03.2015.
+ */
+public abstract class AbstractMealController {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractMealController.class);
+
+ @Autowired
+ private MealService service;
+
+ public Meal get(int id) {
+ int userId = AuthorizedUser.id();
+ LOG.info("get meal {} for User {}", id, userId);
+ return service.get(id, userId);
+ }
+
+ public void delete(int id) {
+ int userId = AuthorizedUser.id();
+ LOG.info("delete meal {} for User {}", id, userId);
+ service.delete(id, userId);
+ }
+
+ public List getAll() {
+ int userId = AuthorizedUser.id();
+ LOG.info("getAll for User {}", userId);
+ return MealsUtil.getWithExceeded(service.getAll(userId), AuthorizedUser.getCaloriesPerDay());
+ }
+
+ public void update(Meal meal, int id) {
+ meal.setId(id);
+ int userId = AuthorizedUser.id();
+ LOG.info("update {} for User {}", meal, userId);
+ service.update(meal, userId);
+ }
+
+ public Meal create(Meal 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 MealsUtil.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/MealAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java
new file mode 100644
index 000000000000..3f03e32c9e82
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java
@@ -0,0 +1,64 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealWithExceed;
+
+import javax.validation.Valid;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+/**
+ * gkislin
+ * 16.10.2016
+ */
+@RestController
+@RequestMapping(value = "/ajax/profile/meals")
+public class MealAjaxController extends AbstractMealController {
+
+/*
+ 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");
+ }
+*/
+
+ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @GetMapping(value = "/{id}")
+ public Meal get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+
+ @DeleteMapping(value = "/{id}")
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @PostMapping
+ public void updateOrCreate(@Valid Meal meal) {
+ if (meal.isNew()) {
+ super.create(meal);
+ } else {
+ super.update(meal, meal.getId());
+ }
+ }
+
+ @PostMapping(value = "/filter", 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);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
new file mode 100644
index 000000000000..2ea0d619cd56
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
@@ -0,0 +1,67 @@
+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.Meal;
+import ru.javawebinar.topjava.to.MealWithExceed;
+
+import javax.validation.Valid;
+import java.net.URI;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = MealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class MealRestController extends AbstractMealController {
+ static final String REST_URL = "/rest/profile/meals";
+
+ @GetMapping("/{id}")
+ public Meal get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+ @DeleteMapping("/{id}")
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @GetMapping
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
+ public void update(@Valid @RequestBody Meal meal, @PathVariable("id") int id) {
+ super.update(meal, id);
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@Valid @RequestBody Meal meal) {
+ Meal created = super.create(meal);
+
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL + "/{id}")
+ .buildAndExpand(created.getId()).toUri();
+
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @GetMapping(value = "/between")
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
new file mode 100644
index 000000000000..af3e4276ee81
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -0,0 +1,84 @@
+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) {
+ checkModificationAllowed(id);
+ 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..dc686ec69cc8
--- /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;
+
+ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ @JsonView(View.UI.class)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+ @JsonView(View.UI.class)
+ public User get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+ @DeleteMapping("/{id}")
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @PostMapping
+ 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()));
+ }
+ }
+
+ @PostMapping(value = "/{id}")
+ 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..b1f49da52e42
--- /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";
+
+ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
+ public User get(@PathVariable("id") int id) {
+ return super.get(id);
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@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);
+ }
+
+ @DeleteMapping(value = "/{id}")
+ public void delete(@PathVariable("id") int id) {
+ super.delete(id);
+ }
+
+ @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
+ public void update(@Valid @RequestBody User user, @PathVariable("id") int id) {
+ super.update(user, id);
+ }
+
+ @GetMapping(value = "/by", 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..4d469897d4b1
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,40 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.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";
+
+ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
+ public User get() {
+ return super.get(AuthorizedUser.id());
+ }
+
+ @DeleteMapping
+ public void delete() {
+ super.delete(AuthorizedUser.id());
+ }
+
+ @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ public void update(@Valid @RequestBody UserTo userTo) {
+ userTo.setId(AuthorizedUser.id());
+ super.update(userTo);
+ }
+
+ @GetMapping(value = "/text")
+ public String testUTF() {
+ return "Русский текст";
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml
new file mode 100644
index 000000000000..b0275e589a4a
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/cache/ehcache.xsd b/src/main/resources/cache/ehcache.xsd
new file mode 100644
index 000000000000..bfc19ddb1e7d
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xsd
@@ -0,0 +1,419 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/db/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..aeeb57462caf
--- /dev/null
+++ b/src/main/resources/db/hsqldb.properties
@@ -0,0 +1,12 @@
+#database.url=jdbc:hsqldb:file:D:/temp/topjava
+database.url=jdbc:hsqldb:mem:topjava
+
+database.username=sa
+database.password=
+
+database.init=true
+
+jdbc.initLocation=initDB_hsql.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql
new file mode 100644
index 000000000000..aede3736c9d0
--- /dev/null
+++ b/src/main/resources/db/initDB.sql
@@ -0,0 +1,36 @@
+DROP TABLE IF EXISTS user_roles;
+DROP TABLE IF EXISTS meals;
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS global_seq;
+
+CREATE SEQUENCE global_seq START 100000;
+
+CREATE TABLE users
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ name VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ password VARCHAR NOT NULL,
+ registered TIMESTAMP DEFAULT now(),
+ enabled BOOL DEFAULT TRUE,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
+
+CREATE TABLE user_roles
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meals (
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ user_id INTEGER NOT NULL,
+ date_time TIMESTAMP NOT NULL,
+ description TEXT NOT NULL,
+ calories INT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals(user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql
new file mode 100644
index 000000000000..3ac041a8d1e8
--- /dev/null
+++ b/src/main/resources/db/initDB_hsql.sql
@@ -0,0 +1,37 @@
+DROP TABLE user_roles IF EXISTS;
+DROP TABLE meals IF EXISTS;
+DROP TABLE users IF EXISTS;
+DROP SEQUENCE global_seq IF EXISTS;
+
+CREATE SEQUENCE GLOBAL_SEQ AS INTEGER START WITH 100000;
+
+CREATE TABLE users
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ name VARCHAR(255),
+ email VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ registered TIMESTAMP DEFAULT now(),
+ enabled BOOLEAN DEFAULT TRUE,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON USERS (email);
+
+CREATE TABLE user_roles
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR(255),
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY ( user_id ) REFERENCES USERS (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meals
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ date_time TIMESTAMP NOT NULL,
+ description VARCHAR(255) NOT NULL,
+ calories INT NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY ( user_id ) REFERENCES USERS (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals (user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql
new file mode 100644
index 000000000000..94f3ad87d28e
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,34 @@
+-- http://stackoverflow.com/questions/13223820/postgresql-delete-all-content
+-- TRUNCATE public CASCADE;
+
+-- http://stackoverflow.com/a/4991969/548473
+-- TRUNCATE SCHEMA public AND COMMIT;
+
+DELETE FROM user_roles;
+DELETE FROM meals;
+DELETE FROM users;
+
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+-- admin
+INSERT INTO users (name, email, password, calories_per_day)
+VALUES ('User', 'user@yandex.ru', '$2a$10$Sh0ZD2NFrzRRJJEKEWn8l.92ROEuzlVyzB9SV1AM8fdluPR0aC1ni', 2005);
+
+-- password
+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..8d4b680629ff
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,14 @@
+#database.url=jdbc:postgresql://ec2-54-217-202-110.eu-west-1.compute.amazonaws.com:5432/dehm6lvm8bink0?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory
+#database.username=wegxlfzjjgxaxy
+#database.password=SSQyKKE_e93kiUCR-ehzMcKCxZ
+
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=user
+database.password=password
+
+database.init=true
+
+jdbc.initLocation=initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties
new file mode 100644
index 000000000000..09ed9c2548ee
--- /dev/null
+++ b/src/main/resources/db/tomcat.properties
@@ -0,0 +1,7 @@
+database.init=false
+
+jdbc.initLocation=initDB.sql
+
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 000000000000..781661c4c06d
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ ${TOPJAVA_ROOT}/log/topjava.log
+
+
+ UTF-8
+ %date %-5level %logger{0} [%file:%line] %msg%n
+
+
+
+
+
+ UTF-8
+ %-5level %logger{0} [%file:%line] %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml
new file mode 100644
index 000000000000..f73d99354efc
--- /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..77885a496c29
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..f0360f900831
--- /dev/null
+++ b/src/main/resources/spring/spring-mvc.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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..d53758c2bb6b
--- /dev/null
+++ b/src/main/resources/spring/spring-security.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..784de2d79e20
--- /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..4ec4a8d5893e
--- /dev/null
+++ b/src/main/resources/tomcat/context.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+ 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..1ee7c6a7761f
--- /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..309bc48f70be
--- /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" %>
+
+
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..f2bbffc338e8
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp
@@ -0,0 +1,17 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp
new file mode 100644
index 000000000000..e9f8663c4a0e
--- /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" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..8880d9e1882f
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/lang.jsp
@@ -0,0 +1,15 @@
+<%@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..9a78c693266f
--- /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/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp
new file mode 100644
index 000000000000..806c831660aa
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/meals.jsp
@@ -0,0 +1,62 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%>
+ <%--<%=TimeUtil.toString(meal.getDateTime())%>--%>
+ ${fn:formatDateTime(meal.dateTime)}
+
+ ${meal.description}
+ ${meal.calories}
+
+
+
+
+
+
+
+
+
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..2e2b13026c01
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/profile.jsp
@@ -0,0 +1,58 @@
+
+<%@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" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${userTo.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp
new file mode 100644
index 000000000000..94740c20792c
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/users.jsp
@@ -0,0 +1,94 @@
+<%@ 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..a28f667d9c93
--- /dev/null
+++ b/src/main/webapp/WEB-INF/tags/inputField.tag
@@ -0,0 +1,22 @@
+<%@ 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" %>
+
+
+
+
\ 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..7921d2914838
--- /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..81e673bf56b1
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,66 @@
+
+ Topjava
+
+
+ spring.profiles.default
+ postgres,datajpa
+
+
+
+ contextConfigLocation
+
+ classpath:spring/spring-app.xml
+ classpath:spring/spring-db.xml
+
+
+
+
+
+ org.springframework.web.context.ContextLoaderListener
+
+
+ mvc-dispatcher
+ org.springframework.web.servlet.DispatcherServlet
+
+ contextConfigLocation
+ classpath:spring/spring-mvc.xml
+
+ 1
+
+
+ mvc-dispatcher
+ /
+
+
+
+ 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..3d12cb93a2ac
--- /dev/null
+++ b/src/main/webapp/resources/js/datatablesUtil.js
@@ -0,0 +1,144 @@
+var form;
+
+function makeEditable() {
+ form = $('#detailsForm');
+ $(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);
+ });
+}
+
+// https://api.jquery.com/jquery.extend/#jQuery-extend-deep-target-object1-objectN
+function extendsOpts(opts) {
+ $.extend(true, opts,
+ {
+ "ajax": {
+ "url": ajaxUrl,
+ "dataSrc": ""
+ },
+ "paging": false,
+ "info": true,
+ "language": {
+ "search": i18n["common.search"]
+ }
+ }
+ );
+ return opts;
+}
+
+function add(add_title) {
+ $('#modalTitle').html(add_title);
+ form.find(":input").val("");
+ $('#editRow').modal();
+}
+
+function updateRow(id) {
+ $('#modalTitle').html(edit_title);
+ $.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('common.deleted');
+ }
+ });
+}
+
+function enable(chkbox, id) {
+ var enabled = chkbox.is(":checked");
+ $.ajax({
+ url: ajaxUrl + id,
+ type: 'POST',
+ data: 'enabled=' + enabled,
+ success: function () {
+ chkbox.closest('tr').fadeTo(300, enabled ? 1 : 0.3);
+ successNoty(enabled ? 'common.enabled' : 'common.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('common.saved');
+ }
+ });
+}
+
+var failedNote;
+
+function closeNoty() {
+ if (failedNote) {
+ failedNote.close();
+ failedNote = undefined;
+ }
+}
+
+function successNoty(key) {
+ closeNoty();
+ noty({
+ text: i18n[key],
+ type: 'success',
+ layout: 'bottomRight',
+ timeout: true
+ });
+}
+
+function failNoty(event, jqXHR, options, jsExc) {
+ closeNoty();
+ var errorInfo = $.parseJSON(jqXHR.responseText);
+ failedNote = noty({
+ text: i18n['common.failed'] + ': ' + jqXHR.statusText + " "+ errorInfo.cause + " " + errorInfo.details.join(" "),
+ type: 'error',
+ layout: 'bottomRight'
+ });
+}
+
+function renderEditBtn(data, type, row) {
+ if (type == 'display') {
+ return ''+i18n['common.update']+' ';
+ }
+}
+
+function renderDeleteBtn(data, type, row) {
+ if (type == 'display') {
+ return ''+i18n['common.delete']+' ';
+ }
+}
+
+function renderEditBtn(data, type, row) {
+ if (type == 'display') {
+ return 'Edit ';
+ }
+}
+
+function renderDeleteBtn(data, type, row) {
+ if (type == 'display') {
+ return '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..e815c757bd26
--- /dev/null
+++ b/src/main/webapp/resources/js/mealDatatables.js
@@ -0,0 +1,88 @@
+var ajaxUrl = 'ajax/profile/meals/';
+var datatableApi;
+
+function updateTable() {
+ $.ajax({
+ type: "POST",
+ url: ajaxUrl + 'filter',
+ data: $('#filter').serialize(),
+ success: updateTableByData
+ });
+}
+
+$(function () {
+ datatableApi = $('#datatable').DataTable(extendsOpts({
+ "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": renderEditBtn
+ },
+ {
+ "defaultContent": "",
+ "orderable": false,
+ "render": renderDeleteBtn
+
+ }
+ ],
+ "order": [
+ [
+ 0,
+ "desc"
+ ]
+ ],
+ "createdRow": function (row, data, dataIndex) {
+ $(row).addClass(data.exceed ? 'exceeded' : 'normal');
+ },
+ "initComplete": makeEditable
+ }));
+
+ $.datetimepicker.setLocale(localeCode);
+
+ var startDate = $('#startDate');
+ var endDate = $('#endDate');
+ startDate.datetimepicker({
+ timepicker: false,
+ format: 'Y-m-d',
+ formatDate: 'Y-m-d',
+ onShow: function (ct) {
+ this.setOptions({
+ maxDate: endDate.val() ? endDate.val() : false
+ })
+ }
+ });
+ endDate.datetimepicker({
+ timepicker: false,
+ format: 'Y-m-d',
+ formatDate: 'Y-m-d',
+ onShow: function (ct) {
+ this.setOptions({
+ minDate: startDate.val() ? startDate.val() : false
+ })
+ }
+ });
+
+ $('#startTime, #endTime').datetimepicker({
+ datepicker: false,
+ format: 'H:i'
+ });
+
+ $('#dateTime').datetimepicker({
+ format: 'Y-m-d H:i'
+ });
+});
diff --git a/src/main/webapp/resources/js/userDatatables.js b/src/main/webapp/resources/js/userDatatables.js
new file mode 100644
index 000000000000..2f5556a94eac
--- /dev/null
+++ b/src/main/webapp/resources/js/userDatatables.js
@@ -0,0 +1,68 @@
+var ajaxUrl = 'ajax/admin/users/';
+var datatableApi;
+
+function updateTable() {
+ $.get(ajaxUrl, updateTableByData);
+}
+
+$(function () {
+ datatableApi = $('#datatable').DataTable(extendsOpts({
+ "columns": [
+ {
+ "data": "name"
+ },
+ {
+ "data": "email",
+ "render": function (data, type, row) {
+ if (type == 'display') {
+ return '' + data + ' ';
+ }
+ return data;
+ }
+ },
+ {
+ "data": "roles"
+ },
+ {
+ "data": "enabled",
+ "render": function (data, type, row) {
+ if (type == 'display') {
+ return ' ';
+ }
+ return data;
+ }
+ },
+ {
+ "data": "registered",
+ "render": function (date, type, row) {
+ if (type == 'display') {
+ return '' + date.substring(0, 10) + ' ';
+ }
+ return date;
+ }
+ },
+ {
+ "orderable": false,
+ "defaultContent": "",
+ "render": renderEditBtn
+ },
+ {
+ "orderable": false,
+ "defaultContent": "",
+ "render": renderDeleteBtn
+ }
+ ],
+ "order": [
+ [
+ 0,
+ "asc"
+ ]
+ ],
+ "createdRow": function (row, data, dataIndex) {
+ if (!data.enabled) {
+ $(row).css("opacity", 0.3);
+ }
+ },
+ "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..62e151e5806f
--- /dev/null
+++ b/src/main/webapp/test.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java
new file mode 100644
index 000000000000..335c9d79343d
--- /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.Meal;
+import ru.javawebinar.topjava.to.MealWithExceed;
+
+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 = ModelMatcher.of(Meal.class);
+ public static final ModelMatcher MATCHER_WITH_EXCEED = ModelMatcher.of(MealWithExceed.class);
+
+ public static final int MEAL1_ID = START_SEQ + 2;
+ public static final int ADMIN_MEAL_ID = START_SEQ + 8;
+
+ public static final Meal MEAL1 = new Meal(MEAL1_ID, of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500);
+ public static final Meal MEAL2 = new Meal(MEAL1_ID + 1, of(2015, Month.MAY, 30, 13, 0), "Обед", 1000);
+ public static final Meal MEAL3 = new Meal(MEAL1_ID + 2, of(2015, Month.MAY, 30, 20, 0), "Ужин", 500);
+ public static final Meal MEAL4 = new Meal(MEAL1_ID + 3, of(2015, Month.MAY, 31, 10, 0), "Завтрак", 500);
+ public static final Meal MEAL5 = new Meal(MEAL1_ID + 4, of(2015, Month.MAY, 31, 13, 0), "Обед", 1000);
+ public static final Meal MEAL6 = new Meal(MEAL1_ID + 5, of(2015, Month.MAY, 31, 20, 0), "Ужин", 510);
+ public static final Meal ADMIN_MEAL1 = new Meal(ADMIN_MEAL_ID, of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510);
+ public static final Meal ADMIN_MEAL2 = new Meal(ADMIN_MEAL_ID + 1, of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500);
+
+ public static final List MEALS = Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1);
+
+ public static Meal getCreated() {
+ return new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Созданный ужин", 300);
+ }
+
+ public static Meal getUpdated() {
+ return new Meal(MEAL1_ID, MEAL1.getDateTime(), "Обновленный завтрак", 200);
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java
new file mode 100644
index 000000000000..17252620c23e
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java
@@ -0,0 +1,44 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.context.support.GenericXmlApplicationContext;
+import ru.javawebinar.topjava.to.MealWithExceed;
+import ru.javawebinar.topjava.web.meal.MealRestController;
+import ru.javawebinar.topjava.web.user.AdminRestController;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.Month;
+import java.util.Arrays;
+import java.util.List;
+
+import static ru.javawebinar.topjava.TestUtil.mockAuthorize;
+import static ru.javawebinar.topjava.UserTestData.USER;
+
+/**
+ * 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();
+
+ mockAuthorize(USER);
+
+ System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames()));
+ AdminRestController adminUserController = appCtx.getBean(AdminRestController.class);
+ adminUserController.get(UserTestData.USER_ID);
+ System.out.println();
+
+ MealRestController mealController = appCtx.getBean(MealRestController.class);
+ List filteredMealsWithExceeded =
+ mealController.getBetween(
+ LocalDate.of(2015, Month.MAY, 30), LocalTime.of(7, 0),
+ LocalDate.of(2015, Month.MAY, 31), LocalTime.of(11, 0));
+ filteredMealsWithExceeded.forEach(System.out::println);
+ }
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/TestUtil.java b/src/test/java/ru/javawebinar/topjava/TestUtil.java
new file mode 100644
index 000000000000..29cf92cd35d7
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java
@@ -0,0 +1,39 @@
+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.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 void mockAuthorize(User user) {
+ SecurityContextHolder.getContext().setAuthentication(
+ new UsernamePasswordAuthenticationToken(new AuthorizedUser(user), null, user.getRoles()));
+ }
+
+ public static RequestPostProcessor userHttpBasic(User user) {
+ return SecurityMockMvcRequestPostProcessors.httpBasic(user.getEmail(), user.getPassword());
+ }
+
+ public static RequestPostProcessor userAuth(User user) {
+ return SecurityMockMvcRequestPostProcessors.authentication(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword()));
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java
new file mode 100644
index 000000000000..c6c878f4c43a
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java
@@ -0,0 +1,49 @@
+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 {
+ private static final Logger LOG = LoggerFactory.getLogger(UserTestData.class);
+
+ 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);
+
+ public static final ModelMatcher MATCHER = ModelMatcher.of(User.class,
+ (expected, actual) -> expected == actual ||
+ (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())
+ )
+ );
+
+ 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;
+ }
+
+}
\ No newline at end of file
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..bdbba1285040
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java
@@ -0,0 +1,130 @@
+package ru.javawebinar.topjava.matcher;
+
+import org.junit.Assert;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+import ru.javawebinar.topjava.TestUtil;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+
+/**
+ * GKislin
+ * 06.01.2015.
+ *
+ * This class wrap every entity by Wrapper before assertEquals in order to compare them by comparator
+ * Default comparator compare by String.valueOf(entity)
+ *
+ * @param : Entity
+ */
+public class ModelMatcher {
+ private static final Comparator DEFAULT_COMPARATOR =
+ (Object expected, Object actual) -> expected == actual || String.valueOf(expected).equals(String.valueOf(actual));
+
+ private Comparator comparator;
+ private Class entityClass;
+
+ public interface Comparator {
+ boolean compare(T expected, T actual);
+ }
+
+ public static ModelMatcher of(Class entityClass) {
+ return new ModelMatcher<>(entityClass);
+ }
+
+ public static ModelMatcher of(Class entityClass, Comparator comparator) {
+ return new ModelMatcher<>(entityClass, comparator);
+ }
+
+ public ModelMatcher(Class entityClass) {
+ this(entityClass, (Comparator) DEFAULT_COMPARATOR);
+ }
+
+ public ModelMatcher(Class entityClass, Comparator comparator) {
+ this.entityClass = entityClass;
+ this.comparator = comparator;
+ }
+
+ private class Wrapper {
+ private T entity;
+
+ private Wrapper(T entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Wrapper that = (Wrapper) o;
+ return entity != null ? comparator.compare(entity, that.entity) : that.entity == null;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(entity);
+ }
+ }
+
+ private T fromJsonValue(String json) {
+ return JsonUtil.readValue(json, entityClass);
+ }
+
+ private Collection fromJsonValues(String json) {
+ return JsonUtil.readValues(json, entityClass);
+ }
+
+ public T fromJsonAction(ResultActions action) throws UnsupportedEncodingException {
+ return fromJsonValue(TestUtil.getContent(action));
+ }
+
+ public void assertEquals(T expected, T actual) {
+ Assert.assertEquals(wrap(expected), wrap(actual));
+ }
+
+ public void assertCollectionEquals(Collection expected, Collection actual) {
+ Assert.assertEquals(wrap(expected), wrap(actual));
+ }
+
+ public Wrapper wrap(T entity) {
+ return new Wrapper(entity);
+ }
+
+ public List wrap(Collection collection) {
+ return collection.stream().map(this::wrap).collect(Collectors.toList());
+ }
+
+ public ResultMatcher contentMatcher(T expect) {
+ return content().string(
+ new TestMatcher(expect) {
+ @Override
+ protected boolean compare(T expected, String body) {
+ Wrapper expectedForCompare = wrap(expected);
+ Wrapper actualForCompare = wrap(fromJsonValue(body));
+ return expectedForCompare.equals(actualForCompare);
+ }
+ });
+ }
+
+ public final ResultMatcher contentListMatcher(T... expected) {
+ return contentListMatcher(Arrays.asList(expected));
+ }
+
+ public final ResultMatcher contentListMatcher(List expected) {
+ return content().string(
+ new TestMatcher>(expected) {
+ @Override
+ protected boolean compare(List expected, String actual) {
+ List expectedList = wrap(expected);
+ List actualList = wrap(fromJsonValues(actual));
+ return expectedList.equals(actualList);
+ }
+ });
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java b/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java
new file mode 100644
index 000000000000..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/InMemoryMealRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryMealRepositoryImpl.java
new file mode 100644
index 000000000000..eec9fb8aad27
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryMealRepositoryImpl.java
@@ -0,0 +1,74 @@
+package ru.javawebinar.topjava.repository.mock;
+
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+import ru.javawebinar.topjava.util.TimeUtil;
+
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * GKislin
+ * 15.09.2015.
+ */
+@Repository
+public class InMemoryMealRepositoryImpl implements MealRepository {
+
+ private static final Comparator MEAL_COMPARATOR = Comparator.comparing(Meal::getDateTime).reversed();
+
+ // Map userId -> (mealId-> meal)
+ private Map> repository = new ConcurrentHashMap<>();
+ private AtomicInteger counter = new AtomicInteger(0);
+
+ @Override
+ public Meal save(Meal meal, int userId) {
+ Integer mealId = meal.getId();
+ if (meal.isNew()) {
+ mealId = counter.incrementAndGet();
+ meal.setId(mealId);
+ } else if (get(mealId, userId) == null) {
+ return null;
+ }
+ Map meals = repository.computeIfAbsent(userId, ConcurrentHashMap::new);
+ meals.put(mealId, meal);
+ return meal;
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ Map meals = repository.get(userId);
+ return meals != null && meals.remove(id) != null;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ Map meals = repository.get(userId);
+ return meals == null ? null : meals.get(id);
+ }
+
+ @Override
+ public Collection getAll(int userId) {
+ return getAllStream(userId).collect(Collectors.toList());
+ }
+
+ @Override
+ public Collection getBetween(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ return getAllStream(userId)
+ .filter(m -> TimeUtil.isBetween(m.getDateTime(), startDateTime, endDateTime))
+ .collect(Collectors.toList());
+ }
+
+ private Stream getAllStream(int userId) {
+ Map meals = repository.get(userId);
+ return meals == null ?
+ Stream.empty() : meals.values().stream().sorted(MEAL_COMPARATOR);
+ }
+}
+
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..77125080c281
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java
@@ -0,0 +1,74 @@
+package ru.javawebinar.topjava.repository.mock;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Repository;
+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 {
+ private static final Logger LOG = LoggerFactory.getLogger(InMemoryUserRepositoryImpl.class);
+
+ private Map repository = new ConcurrentHashMap<>();
+ private AtomicInteger counter = new AtomicInteger(0);
+
+ private static final Comparator USER_COMPARATOR = Comparator.comparing(User::getName).thenComparing(User::getEmail);
+
+ @Override
+ public User save(User user) {
+ 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) {
+ 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..eb8f8e9ea70e
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java
@@ -0,0 +1,21 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.Before;
+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;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ jpaUtil.clear2ndLevelHibernateCache();
+ }
+}
diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java
new file mode 100644
index 000000000000..67edd3e0f157
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.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.Meal;
+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;
+
+public abstract class AbstractMealServiceTest extends AbstractServiceTest {
+
+ @Autowired
+ protected MealService service;
+
+ @Test
+ public void testDelete() throws Exception {
+ service.delete(MEAL1_ID, USER_ID);
+ MATCHER.assertCollectionEquals(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2), service.getAll(USER_ID));
+ }
+
+ @Test
+ public void testDeleteNotFound() throws Exception {
+ thrown.expect(NotFoundException.class);
+ service.delete(MEAL1_ID, 1);
+ }
+
+ @Test
+ public void testSave() throws Exception {
+ Meal created = getCreated();
+ service.save(created, USER_ID);
+ MATCHER.assertCollectionEquals(Arrays.asList(created, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1), service.getAll(USER_ID));
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID);
+ MATCHER.assertEquals(ADMIN_MEAL1, actual);
+ }
+
+ @Test
+ public void testGetNotFound() throws Exception {
+ thrown.expect(NotFoundException.class);
+ service.get(MEAL1_ID, ADMIN_ID);
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ Meal updated = getUpdated();
+ service.update(updated, USER_ID);
+ MATCHER.assertEquals(updated, service.get(MEAL1_ID, USER_ID));
+ }
+
+ @Test
+ public void testNotFoundUpdate() throws Exception {
+ Meal 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(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/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java
new file mode 100644
index 000000000000..f7a25ed77a59
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java
@@ -0,0 +1,61 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.AfterClass;
+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"))
+abstract public class AbstractServiceTest {
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractServiceTest.class);
+
+ private static StringBuilder results = new StringBuilder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev
+ public Stopwatch stopwatch = new Stopwatch() {
+
+ @Override
+ protected void finished(long nanos, Description description) {
+ String result = String.format("%-25s %7d", description.getMethodName(), TimeUnit.NANOSECONDS.toMillis(nanos));
+ results.append(result).append('\n');
+ LOG.info(result + " ms\n");
+ }
+ };
+
+ @AfterClass
+ public static void printResults() {
+ results = new StringBuilder("\n---------------------------------")
+ .append("\nTest Duration, ms")
+ .append("\n---------------------------------\n")
+ .append(results)
+ .append("---------------------------------\n");
+ LOG.info(results.toString());
+ results.setLength(0);
+ }
+}
\ 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..97aaca8e1139
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java
@@ -0,0 +1,92 @@
+package ru.javawebinar.topjava.service;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataAccessException;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import static ru.javawebinar.topjava.UserTestData.*;
+
+public abstract class AbstractUserServiceTest extends AbstractServiceTest {
+
+ @Autowired
+ protected UserService service;
+
+ @Before
+ public void setUp() throws Exception {
+ service.evictCache();
+ }
+
+ @Test
+ public void testSave() throws Exception {
+ User newUser = new User(null, "New", "new@gmail.com", "newPass", 1555, false, 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", 2000, Role.ROLE_USER));
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ service.delete(USER_ID);
+ MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), service.getAll());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testNotFoundDelete() throws Exception {
+ service.delete(1);
+ }
+
+ @Test
+ public void testGet() throws Exception {
+ User user = service.get(ADMIN_ID);
+ MATCHER.assertEquals(ADMIN, user);
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetNotFound() throws Exception {
+ service.get(1);
+ }
+
+ @Test
+ public void testGetByEmail() throws Exception {
+ User user = service.getByEmail("admin@gmail.com");
+ MATCHER.assertEquals(ADMIN, user);
+ }
+
+ @Test
+ public void testGetAll() throws Exception {
+ Collection all = service.getAll();
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, USER), all);
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ User updated = new User(USER);
+ updated.setName("UpdatedName");
+ updated.setCaloriesPerDay(330);
+ updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN));
+ service.update(updated);
+ MATCHER.assertEquals(updated, service.get(USER_ID));
+ }
+
+ @Test
+ public void testSetEnabledEquals() {
+ service.enable(USER_ID, false);
+ Assert.assertFalse(service.get(USER_ID).isEnabled());
+ service.enable(USER_ID, true);
+ Assert.assertTrue(service.get(USER_ID).isEnabled());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java
new file mode 100644
index 000000000000..cfb91aeff0b0
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java
@@ -0,0 +1,27 @@
+package ru.javawebinar.topjava.service.datajpa;
+
+import org.junit.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.UserTestData;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.service.AbstractMealServiceTest;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.MealTestData.*;
+import static ru.javawebinar.topjava.Profiles.DATAJPA;
+import static ru.javawebinar.topjava.UserTestData.ADMIN_ID;
+
+@ActiveProfiles(DATAJPA)
+public class DataJpaMealServiceTest extends AbstractMealServiceTest {
+ @Test
+ public void testGetWithUser() throws Exception {
+ Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID);
+ MATCHER.assertEquals(ADMIN_MEAL1, adminMeal);
+ UserTestData.MATCHER.assertEquals(UserTestData.ADMIN, adminMeal.getUser());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetWithUserNotFound() throws Exception {
+ service.getWithUser(MEAL1_ID, ADMIN_ID);
+ }
+}
\ 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..160ea9572d92
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java
@@ -0,0 +1,26 @@
+package ru.javawebinar.topjava.service.datajpa;
+
+import org.junit.Test;
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.MealTestData;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import static ru.javawebinar.topjava.Profiles.DATAJPA;
+import static ru.javawebinar.topjava.UserTestData.*;
+
+@ActiveProfiles(DATAJPA)
+public class DataJpaUserServiceTest extends AbstractJpaUserServiceTest {
+ @Test
+ public void testGetWithMeals() throws Exception {
+ User user = service.getWithMeals(USER_ID);
+ MATCHER.assertEquals(USER, user);
+ MealTestData.MATCHER.assertCollectionEquals(MealTestData.MEALS, user.getMeals());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testGetWithMealsNotFound() throws Exception {
+ service.getWithMeals(1);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java
new file mode 100644
index 000000000000..9ff4ae615b2b
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jdbc;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractMealServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JDBC;
+
+@ActiveProfiles(JDBC)
+public class JdbcMealServiceTest extends AbstractMealServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java
new file mode 100644
index 000000000000..419f68ed1098
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jdbc;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractUserServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JDBC;
+
+@ActiveProfiles(JDBC)
+public class JdbcUserServiceTest extends AbstractUserServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java
new file mode 100644
index 000000000000..70e7bf865421
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jpa;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractMealServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JPA;
+
+@ActiveProfiles(JPA)
+public class JpaMealServiceTest extends AbstractMealServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java
new file mode 100644
index 000000000000..ffb1fea03c22
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java
@@ -0,0 +1,10 @@
+package ru.javawebinar.topjava.service.jpa;
+
+import org.springframework.test.context.ActiveProfiles;
+import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest;
+
+import static ru.javawebinar.topjava.Profiles.JPA;
+
+@ActiveProfiles(JPA)
+public class JpaUserServiceTest extends AbstractJpaUserServiceTest {
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java
new file mode 100644
index 000000000000..b09e1ef9c424
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java
@@ -0,0 +1,71 @@
+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);
+ }
+
+ @Autowired
+ private JpaUtil jpaUtil;
+
+ protected MockMvc mockMvc;
+
+ @Autowired
+ protected UserService userService;
+
+ @Autowired
+ private WebApplicationContext webApplicationContext;
+
+ @PostConstruct
+ private void postConstruct() {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(webApplicationContext)
+ .addFilter(CHARACTER_ENCODING_FILTER)
+ .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/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java
new file mode 100644
index 000000000000..dd4c6fa11d88
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java
@@ -0,0 +1,45 @@
+package ru.javawebinar.topjava.web;
+
+import org.junit.Test;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+import static ru.javawebinar.topjava.TestUtil.userAuth;
+import static ru.javawebinar.topjava.UserTestData.ADMIN;
+import static ru.javawebinar.topjava.UserTestData.USER;
+
+/**
+ * GKislin
+ * 10.04.2015.
+ */
+
+public class RootControllerTest extends AbstractControllerTest {
+
+ @Test
+ public void testUsers() throws Exception {
+ mockMvc.perform(get("/users")
+ .with(userAuth(ADMIN)))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(view().name("users"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp"));
+ }
+
+ @Test
+ public void testUnAuth() throws Exception {
+ mockMvc.perform(get("/users"))
+ .andDo(print())
+ .andExpect(status().is3xxRedirection())
+ .andExpect(redirectedUrl("http://localhost/login"));
+ }
+
+ @Test
+ public void testMeals() throws Exception {
+ mockMvc.perform(get("/meals")
+ .with(userAuth(USER)))
+ .andDo(print())
+ .andExpect(view().name("meals"))
+ .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java
new file mode 100644
index 000000000000..723d38bc1409
--- /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.Meal;
+
+import java.util.List;
+
+/**
+ * GKislin
+ * 22.07.2015.
+ */
+public class JsonUtilTest {
+
+ @Test
+ public void testReadWriteValue() throws Exception {
+ String json = JsonUtil.writeValue(MealTestData.ADMIN_MEAL1);
+ System.out.println(json);
+ Meal meal = JsonUtil.readValue(json, Meal.class);
+ MealTestData.MATCHER.assertEquals(MealTestData.ADMIN_MEAL1, meal);
+ }
+
+ @Test
+ public void testReadWriteValues() throws Exception {
+ String json = JsonUtil.writeValue(MealTestData.MEALS);
+ System.out.println(json);
+ List meal = JsonUtil.readValues(json, Meal.class);
+ MealTestData.MATCHER.assertCollectionEquals(MealTestData.MEALS, meal);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java
new file mode 100644
index 000000000000..8dd0df824150
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java
@@ -0,0 +1,178 @@
+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.MealTestData;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.service.MealService;
+import ru.javawebinar.topjava.util.MealsUtil;
+import ru.javawebinar.topjava.web.AbstractControllerTest;
+import ru.javawebinar.topjava.web.json.JsonUtil;
+
+import java.time.LocalDateTime;
+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 MealRestControllerTest extends AbstractControllerTest {
+
+ private static final String REST_URL = MealRestController.REST_URL + '/';
+
+ @Autowired
+ private MealService 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(MealTestData.ADMIN_MEAL1));
+ }
+
+ @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().isUnprocessableEntity());
+ }
+
+ @Test
+ public void testDeleteNotFound() throws Exception {
+ mockMvc.perform(delete(REST_URL + ADMIN_MEAL_ID)
+ .with(userHttpBasic(USER)))
+ .andDo(print())
+ .andExpect(status().isUnprocessableEntity());
+ }
+
+ @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 {
+ Meal 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 testUpdateInvalid() throws Exception {
+ Meal invalid = new Meal(MEAL1_ID, null, null, 6000);
+ mockMvc.perform(put(REST_URL + MEAL1_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(invalid))
+ .with(userHttpBasic(USER)))
+ .andDo(print())
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ public void testUpdateHtmlUnsafe() throws Exception {
+ Meal invalid = new Meal(MEAL1_ID, LocalDateTime.now(), "", 200);
+ mockMvc.perform(put(REST_URL + MEAL1_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(invalid))
+ .with(userHttpBasic(USER)))
+ .andDo(print())
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ public void testCreate() throws Exception {
+ Meal created = getCreated();
+ ResultActions action = mockMvc.perform(post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(created))
+ .with(userHttpBasic(ADMIN)));
+
+ Meal returned = MATCHER.fromJsonAction(action);
+ created.setId(returned.getId());
+
+ MATCHER.assertEquals(created, returned);
+ MATCHER.assertCollectionEquals(Arrays.asList(ADMIN_MEAL2, created, ADMIN_MEAL1), service.getAll(ADMIN_ID));
+ }
+
+ @Test
+ public void testCreateInvalid() throws Exception {
+ Meal invalid = new Meal(null, null, "Dummy", 200);
+ mockMvc.perform(post(REST_URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(JsonUtil.writeValue(invalid))
+ .with(userHttpBasic(ADMIN)))
+ .andDo(print())
+ .andExpect(status().isBadRequest());
+ }
+
+ @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(MealsUtil.getWithExceeded(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(
+ MealsUtil.createWithExceed(MEAL4, true),
+ MealsUtil.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(
+ MealsUtil.createWithExceed(MEAL4, true),
+ MealsUtil.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(
+ MealsUtil.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..267a59effeff
--- /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.*;
+
+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())
+// https://jira.spring.io/browse/SPR-14472
+ .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().isUnprocessableEntity())
+ .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().isUnprocessableEntity())
+ .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", 2300, 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..96684ed191dd
--- /dev/null
+++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java
@@ -0,0 +1,70 @@
+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());
+
+ MATCHER.assertEquals(UserUtil.updateFromTo(new User(USER), updatedTo), userService.getByEmail("newemail@ya.ru"));
+ }
+
+ @Test
+ public void testUpdateInvalid() 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/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..c7c9f62cc5d8
--- /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