diff --git a/.codacy.yml b/.codacy.yml
new file mode 100644
index 000000000000..311a8f4e0029
--- /dev/null
+++ b/.codacy.yml
@@ -0,0 +1,5 @@
+---
+exclude_paths:
+ - 'src/main/webapp/**'
+ - '**.md'
+ - '**.sql'
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000000..c3539a9a5a14
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,26 @@
+# https://docs.travis-ci.com/user/languages/java/
+language: java
+jdk: openjdk16
+
+#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
+
+# https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches
+branches:
+ only:
+ - master
+
+# https://stackoverflow.com/a/49852690/548473:
+services:
+ - postgresql
+
+# https://docs.travis-ci.com/user/notifications#Configuring-email-notifications
+#notifications:
+# email: false
\ No newline at end of file
diff --git a/README.md b/README.md
index 228b708b7094..2941a385669b 100644
--- a/README.md
+++ b/README.md
@@ -41,9 +41,16 @@ Java Enterprise Online Project
- Git Overview
- [Основы Git за 20 минут](https://www.youtube.com/watch?v=TMeZGvtQnT8)
- [Git - для новичков](https://www.youtube.com/watch?list=PLY4rE9dstrJyTdVJpv7FibSaXB4BHPInb&v=PEKN8NtBDQ0)
+ - [Руководство по написанию комментариев в коммитах](https://techrocks.ru/2019/12/02/writing-good-commit-messages)
##  3. Работа с проектом (выполнять инструкции)
-**ВНИМАНИЕ: выбирайте для проекта простой пусть без пробелов и русских букв, например (Windows) `c:\projects\topjava\`. Иначе впоследствии будут проблемы**
+- **ВНИМАНИЕ: выбирайте для проекта простой пусть без пробелов и русских букв, например (Windows) `c:\projects\topjava\`. Иначе впоследствии будут проблемы**
+- **Плагин уже Git Intergation не требуется и вкладку `Version control` в IDEA переименовали в `Git`**
+
+Для переключения режима отображения изменений из вкладки Commit в Git: Local Changes нужно переключить `Settings/Preferences | Version Control | Commit | Use non-modal commit interface` или в контекстном меню вкладки `Commit`:
+
+ 
+
### Патч [prepare_to_HW0.patch](https://drive.google.com/file/d/1LNPpu9OkuCpfpD8ZJHO-o0vwu49p2i5M) (скачать и положить в каталог вашего проекта)
> Проект постоянно улучшается, поэтому видео иногда отличается от кода проекта. Изменения указываю после видео:
@@ -51,7 +58,7 @@ Java Enterprise Online Project
> - в `UserMeals/UserMealWithExcess` поля изменились на `private`
> - обновил данные `UserMealsUtil.meals` и переименовал некоторые пременные, поля и методы
> - добавил `UserMealWithExcess.toString()` и метод для выполнения _Optional домашнего задания_
-
+> - метод фильтрации в `TimeUtil` переименовали в `isBetweenHalfOpen` (также изменилась логика сравнения - `startTime` включается в интервал)
## Инструкция по шагам (из видео):
- Установить ПО (git, JDK8, IntelliJ IDEA, Maven)
@@ -69,6 +76,9 @@ Java Enterprise Online Project
- Выполнить задание и залить на GitHub (commit + push)
- Переключиться в основную ветку проекта master.
+##  4. [Тех.задание: библия или допускаются изменения. Полуоткрытый интервал.](https://drive.google.com/file/d/1BpTzjNFjS0TSekCyt_xvt6YoLvuw5KTZ/view)
+- [Типы промежутков](https://ru.wikipedia.org/wiki/Промежуток_(математика))
+
##  Домашнее задание HW0
```
Реализовать метод `UserMealsUtil.filteredByCycles` через циклы (`forEach`):
@@ -82,7 +92,7 @@ Java Enterprise Online Project
- Оцените Time complexity алгоритма. Если она больше O(N), например O(N*N) или N*log(N), сделайте O(N).
```
- Java 8 Date and Time API
-- Алгоритмы и структуры данных для начинающих: сложность алгоритмов
+- Алгоритмы и структуры данных для начинающих: сложность алгоритмов
- [Головач: сложность алгоритмов в теме коллекций](https://www.youtube.com/watch?v=Ek9ijOiplNE&feature=youtu.be&t=778)
- Time complexity
- Временная сложность алгоритма
@@ -98,27 +108,32 @@ Java Enterprise Online Project
- Java 8: Lambda выражения
- Java 8: Потоки
- Pуководство по Java 8 Stream
-- Java 8 Stream API в картинках и примерах
+- [Полное руководство по Java 8 Stream API в картинках и примерах](https://annimon.com/article/2778)
- [7 способов использовать groupingBy в Stream API](https://habrahabr.ru/post/348536)
- Лямбда-выражения в Java 8
- A Guide to Java 8
- Шпаргалка Java Stream API
- Алексея Владыкин: Элементы функционального программирования в Java
- Yakov Fain о новом в Java 8
-- stream.map vs forEach
+- stream.map vs forEach`
- - без циклов по другим коллекциям
- - решение должно быть рабочим в общем случае (не только при запуске main)
-- через Stream API за 1 проход по исходному списку `meals.streem()`
- - нельзя использовать внешние коллекции, не являющиеся частью коллектора или 2 раза проходить по исходному списку (его копиям).
- Т.е. в решении не должно быть 2 раза `meal.stream()` (в том числе неявно, в составных коллекторах)
- - возможно дополнительные проходы по частям списка
+ - без циклов по другим коллекциям/массивам (к ним также относим методы коллекций `addAll()/removeAll()`)
+ - решение должно быть рабочим в общем случае (работать в приложении с многими пользователями, не только при запуске main)
+- через Stream API за 1 проход по исходному списку `meals.stream()`
+ - нельзя использовать внешние коллекции, не являющиеся частью коллектора
+ - нельзя 2 раза проходить по исходному списку (в том числе его отфильтрованной или преобразованной копии)
+ - возможно дополнительные проходы по частям списка, при этом превышение должно считаться один раз для всего подсписка. Те например нельзя разбить список на на 2 подсписка с четными и нечетными датами и затем их объединить, с подсчетом превышения для каждого элемента.
+
+
+Ресурсы:
+- [Java 8 Stream API, часть шестая: собственный коллектор](https://easyjava.ru/java/language/java-8-stream-api-chast-shestaya-sobstvennyj-kollektor)
+- [Руководство по Java 8 Stream API: Collector](https://annimon.com/article/2778#collector)
### Замечания по использованию Stream API:
- Когда встречаешь что-то непривычное, приходится перестраивать мозги. Например, переход с процедурного на ООП программирование дается непросто. Те, кто не знает шаблонов (и не хотят учить) также их встречают плохо. Хорошая новость в том, что если это принять и начать использовать, то начинаешь получать от этого удовольствие. И тут главное не впасть в другую крайность:
@@ -133,7 +148,7 @@ Java Enterprise Online Project
##  Замечания к HW0
- 1: Код проекта менять можно! Одна из распространенных ошибок как в тестовых заданиях на собеседовании, так и при работе на проекте, что ничего нельзя менять. Конечно при правках в рабочем проекте обязательно нужно проконсультироваться/проревьюироваться у авторов кода (находится по истории VCS)
-- 2: Наследовать `UserMealWithExcess` от `UserMeal` я не буду, т.к. это разные сущности: Transfer Object и Entity. Мы будет их проходить на 2м уроке.
+- 2: Наследовать `UserMealWithExcess` от `UserMeal` нельзя, т.к. это разные сущности: Transfer Object и Entity. Мы будет их проходить на 2м уроке. Это относится и к зависимости.
- 3: Правильная реализация должна быть простой и красивой, можно сделать 2-мя способами: через стримы и через циклы. Сложность должна быть O(N), т.е. без вложенных стримов и циклов.
- 4: При реализации через циклы посмотрите в `Map` на методы `getOrDefault` или `merge`
- 5: **При реализации через `Stream` заменяйте `forEach` оператором `stream.map(..)`**
@@ -144,13 +159,13 @@ Java Enterprise Online Project
- 10: `System.out.println` нельзя делать нигде, кроме как в `main`. Позже введем логирование.
- 11: Результаты, возвращаемые `UserMealsUtil.filteredByStreams` мы будем использовать [в нашем приложении](http://topjava.herokuapp.com/) для фильтрации по времени и отображения еды правильным цветом.
- 12: Обращайте внимание на комментарии к вашим коммитам в git. Они должны быть короткие и информативные (лучше на english)
-- 13: Не полагайтесь в решении на то, что список будет подаваться отсортированным. Такого условия нет.
+- 13: Не полагайтесь в решении на то, что список еды будет подаваться отсортированным. Такого условия нет.
-----
## [Пример 7-го занятия онлайн стажировки, несколько видео открыто](https://github.com/JavaOPs/topjava/blob/master/doc/lesson07.md)
### Полезные ресурсы
> ВНИМАНИЕ:
-> - **ДЗ первого урока будет связано с [созданием небольшого CRUD приложения (в памяти, без DB) на JSP и сервлетах](http://danielniko.com/2012/04/17/simple-crud-using-jsp-servlet-and-mysql/)**. Введение будет, но предварительное знакомство не помешает.
+> - **ДЗ первого урока будет связано с [созданием небольшого CRUD приложения (в памяти, без DB) на JSP и сервлетах](https://danielniko.wordpress.com/2012/04/17/simple-crud-using-jsp-servlet-and-mysql/)**. Введение будет, но предварительное знакомство не помешает.
> - основы JavaSсript необходимы для понимания проекта, начиная с 8-го занятия!
Все остальное - опционально.
@@ -172,17 +187,18 @@ Java Enterprise Online Project
#### Java (базовые вещи)
- Интуит. Программирование на Java
- 1й урок MasterJava: Многопоточность
-- Основы Java garbage collection
+- [Основы Java garbage collection](http://web.archive.org/web/20180831013112/https://ggenikus.github.io/blog/2014/05/04/gc)
- Размер Java объектов
- Введение в Java Reflection API
- Структуры данных в картинках
- Обзор java.util.concurrent.*
-- Синхронизация потоков
+- Синхронизация потоков
- String literal pool
- Маленькие хитрости Java
- A Guide to Java 8
### Туториалы, разное
+- [Открытый курс: Spring Boot + HATEOAS](https://javaops.ru/view/bootjava)
- [Что нужно знать о бэкенде новичку в веб-разработке](https://tproger.ru/translations/backend-web-development)
- [Туториалы: Spring Framework, Hibernate, Java Core, JDBC](http://proselyte.net/tutorials/)
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index f4e7412eb816..7c03206fe59c 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -1,8 +1,45 @@
# TopJava Release Notes
+
+### Topjava 21
+- **добавили документирование REST API: Swagger**
+- мигрировали на JDK 15 и используем текстовые блоки
+- Вынес `produces = MediaType.APPLICATION_JSON_VALUE` на уровень контроллеров
+- Правильно используем [глабальные переменные в js](https://stackoverflow.com/a/5064235/548473)
+- Зарефакторил `inputField.tag`
+- Тестовые переменные переименовал из UPPERCASE в camelCase
+- Из тестов сервисов убрал `throws Exception` (в IDEA больше не генерятся по умолчанию)
+- **Мигрировали на Spring Boot 2.4.1**
+
+### Topjava 20
+- мигрировали на JDK 14
+- в `@SafeHtml` запрещаем весь html (`whitelistType = NONE`)
+- в `topjava.common.js` в `makeEditable()` вместо объекта контекст передаю 3 параметра
+- в UI контроллерах убрал префикс `ajax`
+- из тестов сервисов убрал `repository`. При проверке через `assertThrows` он не требуется
+- в `TestMatcher` сценарии сравнения сделал параметризируемыми (паттерн стратегия)
+- в API добавили `/users/{id}/with-meals` (см. [двунаправленные отношения](https://www.codeflow.site/ru/article/jackson-bidirectional-relationships-and-infinite-recursion))
+- добавил `UserTestData.USER_WITH_MEALS_MATCHER` (проверки пользователя сразу с едой) и константу id `NOT_FOUND`
+
+### Topjava 19
+- Изменилась логика для интервалов времени (исключаем `endTime`)
+- Заменил собственный `MessageUtil` велосипед на спринговый `MessageSourceAccessor`
+- В ролях убрал префиксы `ROLE_` ([Role and GrantedAuthority](https://stackoverflow.com/a/19542316/548473))
+- Добавился удобный метод `int AbstractBaseEntity.id()`
+- Фикс `Location` в `ProfileRestController.register`
+- Фикс валидации `UniqueMailValidator` для REST update без `user.id`
+- Заменил `jdbc.initLocation` на полный путь - IDEA не ругается
+- В конфигурации `cargo-maven2-plugin` сделал [индивидуальный контекст приложения](https://stackoverflow.com/a/60797999/548473)
+- Тесты
+ - Обновил даты еды на 2020г.
+ - Зарефакторил тесты сервисов на удаление - `NotFoundException` может бросаться при `delete()`
+ - В тестах контроллеров вернулся к реализации без обертки над `MockMvcRequestBuilders`
+ - Для `InMemory` тестов подключаю только `inmemory.xml` (добавил туда необходимую конфигурацию из `spring-app.xml`)
+
+
### Topjava 18
- В `ErrorType` добавил `HttpStatus status`
-- В PostgreSQL обнаружилась бага: граничное значение `0:00` из за ошибок округления попадает в предыдущий интервал.
+- В PostgreSQL обнаружилась бага: граничное значение `0:00` из-за ошибок округления попадает в предыдущий интервал.
Мораль: всегда в тестах проверяйте граничные значения. Добавил этот случай в тестовые данные.
- Изменил `MealRepository.getBetween` (принимаю `@Nullable LocalDate`). Изменились реализации.
- Выделил метод `UserService.prepareAndSave`
diff --git a/cv.md b/cv.md
index a24d881fdeba..f459060b227a 100644
--- a/cv.md
+++ b/cv.md
@@ -20,6 +20,7 @@
### Наши истории (делимся опытом и успехом)
### Тесты/задачи онлайн:
+- [Interviewing: the most profitable skill you can learn (pramp.com)](https://www.pramp.com/)
- [Java Programming Test](https://tests4geeks.com/java)
- game: test Java skills
- Codility lesson tests
@@ -28,7 +29,6 @@
- Sphere online judge
- Codility programmers lessons
- Hackerrank practice coding
-- [Interviewing: the most profitable skill you can learn (pramp.com)](https://www.pramp.com/)
- [start.interviewing.io](https://start.interviewing.io/)
## [Тестовое собеседование, самые спрашиваемые темы](http://javaops.ru/interview/test.html)
@@ -37,6 +37,8 @@
- Михаил Портнов. Собеседование на работу: как продать себя грамотно
- Михаил Портнов. Какие вопросы мы задаем на собеседовании?
- Михаил Портнов. Собеседование на работу: жизненный путь
+- [Лёша Корепанов. Признаки плохих компаний для программиста](https://www.youtube.com/watch?v=Sj-WSWr-n7U)
+- [Лёша Корепанов. Как отвечать на вопросы, которые ты не знаешь. Техническое интервью для программиста](https://www.youtube.com/watch?v=Beoh3tfgPEk)
- Канал: Резюме, поиск работы, интервью
- Яков Файн: Как стать профессиональным Java разработчиком
- Ответы на вопросы на собеседовании Junior Java Developer
@@ -88,9 +90,13 @@
- Яндекс агрегатор
- HH
- LinkedIn
+- ХабрКарьера
+- [headz.io](https://app.headz.io/candidates/new)
- djinni.co (более актуально для Украины)
-## Как выжить на испытательном сроке
+
+
Как выжить на испытательном сроке
+
- Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В
процессе формулирования очень часто приходит ее решение.
- Учись инвестигировать проблему. Внимательное чтение логов и умение дебажить - основные навыки
@@ -106,6 +112,12 @@
- Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи.
- [**5 вещей, которые разработчик должен сделать прежде чем попросить о помощи**](https://techrocks.ru/2018/07/16/5-things-a-developer-should-do-before-asking-for-help/)
- [**Советы новичкам**](http://blog.csssr.ru/2016/09/19/how-to-be-a-beginner-developer)
+- [ТОП-13 ошибок начинающего программиста](https://proglib.io/p/beginners-fails/)
+- [25 ошибок начинающего программиста](https://habr.com/ru/post/413129/)
+- [Путеводитель по синдрому самозванца](https://vc.ru/hr/167443-eshche-odin-putevoditel-po-sindromu-samozvanca-korni-prichiny-simptomy-i-posledstviya-chast-1)
- [Нетехнические навыки](https://tproger.ru/experts/softskills-for-job)
-
+- Видео [Junior и испытательный срок на первой работе](https://www.youtube.com/watch?v=GsGlsCbok-c)
+- Типичные ошибки начинающих программистов от JavaRush:
+ - [Часть 1](https://javarush.ru/groups/posts/3044-razbor-tipichnihkh-oshibok-nachinajujshikh-programmistov-chastjh-1)
+ - [Часть 2](https://javarush.ru/groups/posts/3055-razbor-tipichnihkh-oshibok-nachinajujshikh-programmistov-chastjh-2)
## [Отзывы по стажировке Topjava](https://vk.com/topic-74381644_30447246)
diff --git a/description.md b/description.md
index d2448ca99e57..0834140f19a7 100644
--- a/description.md
+++ b/description.md
@@ -1,77 +1,80 @@
#### Разработка полнофункционального 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 и сохранением в базах данных Postgresql и HSQLDB.
-- Основное внимание будет уделяться способам решения многочисленных проблем разработки в Spring/JPA, а также структурному (красивому и надежному) java кодированию и архитектуре приложения.
-- Каждая итерация проекта закрепляется домашним заданием по реализации схожей функциональности. Следующее занятие начинается с разбора домашних заданий.
-- Большое внимание уделяется тестированию кода: в проекте более 100 JUnit тестов.
-- Несмотря на относительно небольшой размер, приложение разрабатывается с нуля как большой проект (например мы используем кэш 2-го уровня Hibernate, настраиваем Jackson для работы с ленивой загрузкой
+- Основное внимание будет уделяться способам решения многочисленных проблем разработки в Spring/JPA, а также структурному (красивому и надежному) java кодированию и архитектуре приложения.
+- Каждая итерация проекта закрепляется домашним заданием по реализации схожей функциональности. Следующее занятие начинается с разбора домашних заданий.
+- Большое внимание уделяется тестированию кода: в проекте более 100 JUnit тестов.
+- Несмотря на относительно небольшой размер, приложение разрабатывается с нуля как большой проект (например, мы используем кэш 2-го уровня Hibernate, настраиваем Jackson для работы с ленивой загрузкой
Hibernate, делаем конверторы для типов LocalDateTime (Java 8 time API).
- Разбираются архитектурные паттерны: слои приложения и как правильно разбивать логику по слоям, когда нужно применять 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.
+- Разбираются архитектурные паттерны: слои приложения и как правильно разбивать логику по слоям, когда нужно применять 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.
+- Системы управления версиями
+- 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.
- [Тестирование JPA сервиса через AssertJ](https://www.youtube.com/watch?v=BlyaXT6tOaw)
-- Поддержка HSQLDB
-- Транзакции
-- Профили Maven и Spring
-- Пул коннектов
-- Spring Data JPA
-- Кэш Hibernate
+- Поддержка 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. jQuery datatables.
-- AJAX. jQuery. Notifications.
-- Spring Security
-- Spring Binding/Validation
-- Работа с datatables через Ajax.
-- Spring Security Test
+- 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. jQuery datatables.
+- AJAX. jQuery. Notifications.
+- Spring Security
+- Spring Binding/Validation
+- Работа с datatables через Ajax.
+- Spring Security Test
- [Кастомизация JSON (@JsonView) и валидации (groups)](https://drive.google.com/open?id=0B9Ye2auQ_NsFRTFsTjVHR2dXczA)
-- Encoding password
-- CSRF (добавление в проект защиты от межсайтовой подделки запроса)
-- form-login. Spring Security Taglib
-- Handler interceptor
-- Spring Exception Handling
-- Смена локали
-- Фильтрация JSON через @JsonView
-- Защита от XSS (Cross Site Scripting)
-- Деплой в Heroku
-- Локализация datatables, ошибок валидации
-- Обработка ошибок 404 (NotFound)
-- Доступ к AuthorizedUser
-- Собеседование. Разработка ПО
+- Encoding password
+- CSRF (добавление в проект защиты от межсайтовой подделки запроса)
+- form-login. Spring Security Taglib
+- Handler interceptor
+- Spring Exception Handling
+- Смена локали
+- Фильтрация JSON с помощью @JsonView
+- Защита от XSS (Cross Site Scripting)
+- Деплой в Heroku
+- Локализация datatables, ошибок валидации
+- Обработка ошибок 404 (NotFound)
+- Доступ к AuthorizedUser
+- Собеседование. Разработка ПО
+
+### Миграция на Spring Boot
+- Основы Spring Boot. Spring Boot maven plugin
+- Lombok, база H2, ApplicationRunner
+- Spring Data REST + HATEOAS
+- Swagger/ OpenAPI 3.0
+- Тестирование и кэширование в Spring Boot
+- Миграция приложения TopJava на Spring Boot
diff --git a/graduation.md b/graduation.md
index 6e4af3e023cc..4aa2c1b00dae 100644
--- a/graduation.md
+++ b/graduation.md
@@ -11,65 +11,83 @@ Build a voting system for deciding where to have lunch.
* Users can vote on which restaurant they want to have lunch at
* Only one vote counted per user
* If user votes again the same day:
- - If it is before 11:00 we asume that he changed his mind.
+ - If it is before 11:00 we assume that he changed his mind.
- If it is after 11:00 then it is too late, vote can't be changed
-Each restaurant provides new menu each day.
+Each restaurant provides a new menu each day.
-As a result, provide a link to github repository. It should contain the code, README.md with API documentation and couple curl commands to test it.
+As a result, provide a link to github repository. It should contain the code, README.md with API documentation and couple curl commands to test it (better - Swagger).
-----------------------------
P.S.: Make sure everything works with latest version that is on github :)
-P.P.S.: Asume that your API will be used by a frontend developer to build frontend on top of that.
+P.P.S.: Assume that your API will be used by a frontend developer to build frontend on top of that.
-----------------------------
###  Рекомендации
-- Если ты закончил [стажировку Topjava](http://javaops.ru/reg/topjava/grd), **cделай новый проект и добавляй туда из Topjava только то что нужно!** Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - **не нужны!**
-- **API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (frontend)**
+- **Сделай новый проект и добавляй туда из Topjava только то что нужно! Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - не нужны!**
+- **Рекомендую переписать проект современно: Spring Boot + Swagger/OpenAPI 3.0. Оптимально подойдет код миграции TopJava на Spring Boot в конце стажировки.**
- **Сначала сделай основной сценарий по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом.**
+- API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (frontend)
-*Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас конечно есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если конечно в ТЗ нет оговорок). Все свои вопросы и предложения и хотелки оформляйете отдельно (в `read.me` например). Если делаете что-то сложнее простейшего случая (например справочник еды)- обязательно напишите в read.me. Как и выбор стратегии кэширования.*
+*Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас, конечно, есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если, конечно, в ТЗ нет оговорок). Все свои вопросы, предложения и хотелки оформляйте отдельно (в `read.me` например). Если делаете что-то сложнее простейшего случая (например, справочник еды) - обязательно напишите в read.me. Как и выбор стратегии кэширования.*
> Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять
_Антуан де Сент-Экзюпери_
-- 1: **читаем ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять**
-- 2: **тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу**
-- 3: **тщательно считайте количество запросов в вашем API для отображения нужной информации**
-- 4: **учитывайте, что пользователей может быть ооочень много, а админов- мало**
-- 5: в проекте (и тестовом задании на работу) в отличие от нашего учебного topjava оставляйте только необходимый для работы приложения код, ничего лишнего:
- - 5.1 НЕ надо делать разные профили базы и работы с ней.
- - 5.2 НЕ надо делать абстрактных контроллеров на всякий случай.
- - 5.3 НЕ надо делать **классов репозиториев и сервисов**, если там нет ничего, кроме делегирования.
- - 5.4 Из потребностей приложения (которую надо самим придумать) реализовывать только очевидные сценарии. Те.- НИЧЕГО ЛИШНЕГО.
-- 6: базу лучше взять без установки (H2 или HSQLDB). Ваше приложение должно сразу запуститься, **без всяких настроек и переменных окружения**
-- 7: по возможности сделать JUnit тесты
-- 8: уделяйте внимание обработке ошибок
-- 9: далаем REST API в соответствии с концепцией REST
- - [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/)
- - 10 Best Practices for Better RESTful API
+- 1: **Читаем ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять**
+- 2: **Тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу**. Самое худшее в коде - обращение в базу в цикле.
+- 3: **Тщательно считайте количество запросов в вашем API для отображения нужной информации**
+- 4: Учитывайте, что **пользователей может быть ОООЧЕНЬ много, а админов - МАЛО**
+- 5: В проекте (и тестовом задании на работу), в отличие от нашего учебного topjava, оставляйте только необходимый для работы приложения код, ничего лишнего:
+ - 5.1 НЕ надо делать разные профили базы и работы с ней
+ - 5.2 НЕ надо делать абстрактных контроллеров на всякий случай
+ - 5.3 НЕ надо делать **классов репозиториев и сервисов**, если там нет ничего, кроме делегирования
+ - 5.4 Из потребностей приложения (которую надо самим придумать) реализовывать только очевидные сценарии. То есть НИЧЕГО ЛИШНЕГО.
+- 6: База Данных
+ - берите без установки (H2 или HSQLDB). Одну!! Ваше приложение должно сразу запуститься, **без всяких настроек и переменных окружения**
+ - сделайте индексы к таблицам. Попробуйте обеспечит UNIQUE (один голос пользователя в день, один уникальный пункт меню в день). Следите за порядком полей в индексе
+ - **историю еды и голосований сделать НУЖНО! Есть базовые вещи, которые закладываются в архитектуру приложения и неочевидные доработки к ТЗ, которых лучше не делать.**
+ - при популировании добавте записи за сегодняшний день - now(), чтобы всегда были актуальные исходные данные
+ - таблицы обычно именуются в единственном числе. Исключение - users, т. к. user - зарезервированное слово. `date`/`timestamp` - зарезервированное слово, лучше избегать их при именовании
+- 7: По возможности сделать JUnit тесты
+- 8: Уделяйте внимание обработке ошибок
+- 9: Делаем REST API в соответствии с концепцией REST (url в общем имеют вид`{ресурс}/{id_ресурсa}[/{подресурс}/{id_подресурсa}][параметры]`)
+ - **[15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/)**
+ - **10 Best Practices for Better RESTful API**
- [REST resource hierarchy](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources)
-- 10: не смешивайте TO и Entity вместе. Лучше всего, если они будут независимыми друг от друга.
-- 11: если приложению в объекте требуется только его id, используйте reference (как мы при сохранении еды вставляем туда юзера)
-- 12: [Use for money in java app](http://stackoverflow.com/a/43051227/548473)
-- 13: **Историю еды и голосований сделать НУЖНО! Есть базовые вещи, которые закладываются в архитектуру приложения и неочевидные доработки к ТЗ, которых лучше не делать.**
+- 10: Не смешивайте TO и Entity вместе. Лучше всего, если они будут независимыми друг от друга.
+- 11: Не размещайте логику приложения и преобразования в TO в слое доступа к DB
+- 12: Если приложению в объекте требуется только его id, используйте reference (как мы при сохранении еды вставляем туда юзера)
+- 13: [Use for money in java app](http://stackoverflow.com/a/43051227/548473)
- 14: Еще раз про [hashCode/equals в Entity](https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma): не делайте сравнение по полям!
-- 15: Название пакетов, имен классов для `model/to/web` достаточно стандартные (например `model/domain`). НЕ надо придумывать своих собственных правил.
-- 16: **Используйте DATA-JPA** (можно без лишней делегации, напрямую из сервиса/контроллера дергать Repository).
-- 17: В DATA-JPA 2.x используются `Optional`. Попробуйте работать с ними, это безопасный способ работать с null значениями (используйте `orElseThrow`).
-- 18: На topjava мы смотрели разные варианты использования, тут делаем максимально просто. С TO многие вещи упрощаются.
-- 19: Проверьте, не торчат ли из кода учебные уши topjava, типа `ProfileRestController.testUTF()`, `AbstractServiceTest.printResult()` или закомментированные `JdbcTemplate`. Назначение этого проекта совсем другое.
-- 20: ORM работает с объектами. [В простейших случаях fk_id как поля допустимы](https://stackoverflow.com/questions/6311776/hibernate-foreign-keys-instead-of-entities), но при этом JPA их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverwrflow в коде обязательна!
+- 15: Название пакетов, имен классов для `model/to/web` достаточно стандартные (например `model/domain`). НЕ надо придумывать своих собственных правил
+- 16: **Используйте DATA-JPA** (можно без лишней делегации, напрямую из сервиса/контроллера дергать Repository)
+- 17: В DATA-JPA 2.x используются `Optional`. Попробуйте работать с ними, это безопасный способ работать с null значениями (используйте `orElseThrow`)
+- 18: На topjava мы смотрели разные варианты использования, тут делаем максимально просто. С TO многие вещи упрощаются
+- 19: Проверьте, не торчат ли из кода учебные уши topjava, типа `ProfileRestController.testUTF()`, `AbstractServiceTest.printResult()` или закомментированные `JdbcTemplate`. Назначение этого проекта совсем другое
+- 20: ORM работает с объектами. [В простейших случаях fk_id как поля допустимы](https://stackoverflow.com/questions/6311776/hibernate-foreign-keys-instead-of-entities), но при этом JPA их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverflow в коде обязательна!
- 21: Проверьте, станет ли код проще с `@AuthenticationPrincipal` (урок 11, Доступ к AuthorizedUser).
-- 22: Не размещайте логику приложения и преобразования в TO в слое доступа к DB
-- 23: Если используете кэширование, **тщательно продумайте, что надо кэшировать (самые частые запросы)**, а что нет (большие или редкозапрашиваемые данные)!
+- 22: Обновление в базе делается через `update`, даже если `delete/insert` сократит java код на несколько строк
+- 23: Кэширование
+ - необязательно, но желательно. Чем проще реализация - тем лучше.
+ - **тщательно продумайте, что надо кэшировать (самые частые запросы)**, а что нет (большие или редко запрашиваемые данные)!
+ - проверьте соответствие ключей к кэшу (параметры кэшируемого метода) с конфигурацией (например в singleNonExpiryCache, heap=1 в кэше может содержаться только ОДНО значение).
+- 24: Валидация
+ - желательна
+ - одних аннотаций недостаточно. Должны быть `@Valid/@Validation`
+ - проверяйте входные данные при `create/update` **в контроллерах!** В TopJava это `ValidationUtil.checkNew()/assureIdConsistent()`
+- 25: `readme.md`:
+ - Если задание на English, описание пишите также на English (тоже самое относится к языку резюме: вакансия на English предполагает ваше резюме на English)
+ - Требуемые примеры `curl` не прячьте, а пишите здесь! Оптимально - ссылка на Swagger.
+ - Проверяют люди с опытом в Java: не надо писать инструкций, как устанавливать Java и Maven:)
+- 26: На управление (CRUD) рестаранами и едой должны быть ОТДЕЛЬНЫЕ контроллеры. Не надо все, что может админ, сваливать в одну кучу!
## Попробуйте подергать свое API по всем типичным сценариям ТЗ!
- Удобно использовать? Можно сделать проще? Например чтобы проголосовать за ресторан залогиненному юзеру достаточно `restorauntId`.
-- Сколько раз пришлось его вызвать API для типичного сценария (нарпимер посмотреть рестораны с едой)?
+- Сколько раз пришлось его вызвать API для типичного сценария (нарпимер посмотреть рестораны с едой на сегодня)?
- Сколько запросов к базе было сделано? Можно ли сократить (например с FETCH/Graph или через кэширование)?
- **API ДОЛЖНО соответствовать принципам REST (см. ссылки выше)**
- **ОБЯЗАТЕЛЬНО: запустите `mvn test`- ошибок быть не должно**
diff --git a/pom.xml b/pom.xml
index 0b1c2896da5b..1a039ceac371 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
ru.javawebinartopjava
- jar
+ war1.0-SNAPSHOT
@@ -12,14 +12,37 @@
http://topjava.herokuapp.com/
- 1.8
+ 17UTF-8UTF-8
+
+ 5.3.8
+ 2.5.2
+
+ 9.0.48
+
+
+ 1.2.3
+ 1.7.30
+
+
+ 42.2.21
+
+ 4.13.2
+ 3.19.0
+
+
+ 5.5.2.Final
+ 6.2.0.Final
+ 3.0.1-b12
+
+
+ 3.9.4topjava
- install
+ packageorg.apache.maven.plugins
@@ -30,15 +53,183 @@
${java.version}
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.3.1
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.2
+
+ -Dfile.encoding=UTF-8
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+ compile
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ runtime
+
+
+
+ javax.annotation
+ javax.annotation-api
+ 1.3.2
+
+
+
+
+ org.springframework
+ spring-context-support
+
+
+ org.springframework.data
+ spring-data-jpa
+ ${spring-data-jpa.version}
+
+
+
+
+ org.hibernate
+ hibernate-core
+ ${hibernate.version}
+
+
+ org.hibernate.validator
+ hibernate-validator
+ ${hibernate-validator.version}
+
+
+
+
+ org.glassfish
+ javax.el
+ ${javax-el.version}
+ provided
+
+
+
+
+ javax.cache
+ cache-api
+ 1.1.0
+
+
+ org.ehcache
+ ehcache
+ runtime
+ ${ehcache.version}
+
+
+ org.glassfish.jaxb
+ jaxb-runtime
+
+
+
+
+
+
+ org.glassfish.jaxb
+ jaxb-runtime
+ 2.4.0-b180830.0438
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 4.0.1
+ provided
+
+
+
+ javax.servlet
+ jstl
+ 1.2
+
+
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+ org.springframework
+ spring-test
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+ hsqldb
+
+
+ org.hsqldb
+ hsqldb
+ 2.6.0
+
+
+
+
+
+ postgres
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+
+
+ org.apache.tomcat
+ tomcat-jdbc
+ ${tomcat.version}
+ provided
+
+
+ org.slf4j
+ jul-to-slf4j
+ ${slf4j.version}
+ runtime
+
+
+
+ true
+
+
+
+
+ org.springframework
+ spring-framework-bom
+ ${spring.version}
+ pom
+ import
+
+
diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java
deleted file mode 100644
index c2f9cc618f7c..000000000000
--- a/src/main/java/ru/javawebinar/topjava/Main.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package ru.javawebinar.topjava;
-
-/**
- * @see Demo application
- * @see Initial project
- */
-public class Main {
- public static void main(String[] args) {
- System.out.format("Hello TopJava Enterprise!");
- }
-}
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..f729daaa8441
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/Profiles.java
@@ -0,0 +1,28 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.util.ClassUtils;
+
+public class Profiles {
+ public static final String
+ JDBC = "jdbc",
+ JPA = "jpa",
+ DATAJPA = "datajpa";
+
+ public static final String REPOSITORY_IMPLEMENTATION = DATAJPA;
+
+ public static final String
+ POSTGRES_DB = "postgres",
+ HSQL_DB = "hsqldb";
+
+ public static final String ACTIVE_DB = POSTGRES_DB;
+ // Get DB profile depending of DB driver in classpath
+ public static String getActiveDbProfile() {
+ if (ClassUtils.isPresent("org.postgresql.Driver", null)) {
+ return POSTGRES_DB;
+ } else if (ClassUtils.isPresent("org.hsqldb.jdbcDriver", null)) {
+ return HSQL_DB;
+ } else {
+ throw new IllegalStateException("Could not find DB driver");
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java
new file mode 100644
index 000000000000..b869d1c6e382
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java
@@ -0,0 +1,36 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.MealTo;
+import ru.javawebinar.topjava.web.meal.MealRestController;
+import ru.javawebinar.topjava.web.user.AdminRestController;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.Month;
+import java.util.Arrays;
+import java.util.List;
+
+public class SpringMain {
+ public static void main(String[] args) {
+ // java 7 automatic resource management (ARM)
+ try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml")) {
+ System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames()));
+ AdminRestController adminUserController = appCtx.getBean(AdminRestController.class);
+ adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ADMIN));
+ System.out.println();
+
+ MealRestController mealController = appCtx.getBean(MealRestController.class);
+ List filteredMealsWithExcess =
+ mealController.getBetween(
+ LocalDate.of(2020, Month.JANUARY, 30), LocalTime.of(7, 0),
+ LocalDate.of(2020, Month.JANUARY, 31), LocalTime.of(11, 0));
+ filteredMealsWithExcess.forEach(System.out::println);
+ System.out.println();
+ System.out.println(mealController.getBetween(null, null, null, null));
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java
new file mode 100644
index 000000000000..2ab30f34f272
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java
@@ -0,0 +1,69 @@
+package ru.javawebinar.topjava.model;
+
+import org.hibernate.Hibernate;
+import org.springframework.data.domain.Persistable;
+import org.springframework.util.Assert;
+
+import javax.persistence.*;
+
+@MappedSuperclass
+// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access
+@Access(AccessType.FIELD)
+public abstract class AbstractBaseEntity implements Persistable {
+ public static final int START_SEQ = 100000;
+
+ @Id
+ @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ)
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq")
+// See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034
+// Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting
+ protected Integer id;
+
+ protected AbstractBaseEntity() {
+ }
+
+ protected AbstractBaseEntity(Integer id) {
+ this.id = id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ @Override
+ public Integer getId() {
+ return id;
+ }
+
+ public int id() {
+ Assert.notNull(id, "Entity must have id");
+ return id;
+ }
+
+ @Override
+ public boolean isNew() {
+ return this.id == null;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ":" + id;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || !getClass().equals(Hibernate.getClass(o))) {
+ return false;
+ }
+ AbstractBaseEntity that = (AbstractBaseEntity) o;
+ return id != null && id.equals(that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id == null ? 0 : id;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java
new file mode 100644
index 000000000000..edff2ab1718f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava.model;
+
+import javax.persistence.Column;
+import javax.persistence.MappedSuperclass;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+
+@MappedSuperclass
+public abstract class AbstractNamedEntity extends AbstractBaseEntity {
+
+ @NotBlank
+ @Size(min = 2, max = 100)
+ @Column(name = "name", nullable = false)
+ protected String name;
+
+ protected AbstractNamedEntity() {
+ }
+
+ protected AbstractNamedEntity(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 super.toString() + '(' + name + ')';
+ }
+}
\ No newline at end of file
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..fe2c242ede78
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java
@@ -0,0 +1,112 @@
+package ru.javawebinar.topjava.model;
+
+import org.hibernate.validator.constraints.Range;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+@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 >= :startDateTime AND m.dateTime < :endDateTime ORDER BY m.dateTime DESC
+ """),
+// @NamedQuery(name = Meal.GET_BETWEEN, query = "SELECT m FROM Meal m WHERE m.user.id=:userId AND m.dateTime >= :startDateTime AND m.dateTime < :endDateTime 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 AbstractBaseEntity {
+ 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
+ private LocalDateTime dateTime;
+
+ @Column(name = "description", nullable = false)
+ @NotBlank
+ @Size(min = 2, max = 120)
+ private String description;
+
+ @Column(name = "calories", nullable = false)
+ @Range(min = 10, max = 5000)
+ private int calories;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ @NotNull
+ 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(int 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/MealWithExcess.java b/src/main/java/ru/javawebinar/topjava/model/MealWithExcess.java
new file mode 100644
index 000000000000..067127662403
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/MealWithExcess.java
@@ -0,0 +1,12 @@
+package ru.javawebinar.topjava.model;
+
+import java.time.LocalDateTime;
+
+public class MealWithExcess extends Meal{
+ private boolean excess;
+
+ public MealWithExcess(LocalDateTime dateTime, String description, int calories, boolean excess) {
+ super(dateTime, description, calories);
+ this.excess = excess;
+ }
+}
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..acb7a276f6dc
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/Role.java
@@ -0,0 +1,6 @@
+package ru.javawebinar.topjava.model;
+
+public enum Role {
+ USER,
+ ADMIN
+}
\ No newline at end of file
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..faded2d499de
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/User.java
@@ -0,0 +1,148 @@
+package ru.javawebinar.topjava.model;
+
+import org.hibernate.annotations.BatchSize;
+import org.hibernate.validator.constraints.Range;
+import org.springframework.util.CollectionUtils;
+
+import javax.persistence.*;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.*;
+
+import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY;
+
+@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 LEFT JOIN FETCH u.roles WHERE u.email=?1"),
+ @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u ORDER BY u.name, u.email"),
+})
+@Entity
+@Table(name = "users")
+public class User extends AbstractNamedEntity {
+
+ public static final String DELETE = "User.delete";
+ public static final String BY_EMAIL = "User.getByEmail";
+ public static final String ALL_SORTED = "User.getAllSorted";
+
+ @Column(name = "email", nullable = false, unique = true)
+ @Email
+ @NotBlank
+ @Size(max = 100)
+ private String email;
+
+ @Column(name = "password", nullable = false)
+ @NotBlank
+ @Size(min = 5, max = 100)
+ private String password;
+
+ @Column(name = "enabled", nullable = false, columnDefinition = "bool default true")
+ private boolean enabled = true;
+
+ @Column(name = "registered", nullable = false, columnDefinition = "timestamp default now()")
+ @NotNull
+ private Date registered = new Date();
+
+ @Enumerated(EnumType.STRING)
+ @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"),
+ uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_roles")})
+ @Column(name = "role")
+ @ElementCollection(fetch = FetchType.EAGER)
+// @Fetch(FetchMode.SUBSELECT)
+ @BatchSize(size = 200)
+ private Set roles;
+
+ @Column(name = "calories_per_day", nullable = false, columnDefinition = "int default 2000")
+ @Range(min = 10, max = 10000)
+ private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY;
+
+ @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
+ @OrderBy("dateTime DESC")
+ private List meals;
+
+ public User() {
+ }
+
+ public User(User u) {
+ this(u.id, u.name, u.email, u.password, u.caloriesPerDay, u.enabled, u.registered, u.roles);
+ }
+
+ public User(Integer id, String name, String email, String password, Role role, Role... roles) {
+ this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), EnumSet.of(role, roles));
+ }
+
+ public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) {
+ super(id, name);
+ this.email = email;
+ this.password = password;
+ this.caloriesPerDay = caloriesPerDay;
+ this.enabled = enabled;
+ this.registered = registered;
+ 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 String getPassword() {
+ return password;
+ }
+
+ public void setRoles(Collection roles) {
+ this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles);
+ }
+
+ public List getMeals() {
+ return meals;
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "id=" + id +
+ ", email=" + email +
+ ", name=" + name +
+ ", enabled=" + enabled +
+ ", roles=" + roles +
+ ", caloriesPerDay=" + caloriesPerDay +
+ '}';
+ }
+}
\ No newline at end of file
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..86beb3fb6ac3
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java
@@ -0,0 +1,27 @@
+package ru.javawebinar.topjava.repository;
+
+import ru.javawebinar.topjava.model.Meal;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+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 desc
+ List getAll(int userId);
+
+ // ORDERED dateTime desc
+ List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, 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..9fecbddaaa19
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java
@@ -0,0 +1,25 @@
+package ru.javawebinar.topjava.repository;
+
+import ru.javawebinar.topjava.model.User;
+
+import java.util.List;
+
+public interface UserRepository {
+ // null if not found, when updated
+ 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();
+ }
+}
\ No newline at end of file
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..9aeef134f236
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java
@@ -0,0 +1,29 @@
+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;
+
+@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);
+
+ @Query("SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC")
+ List getAll(@Param("userId") int userId);
+
+ @Query("SELECT m from Meal m WHERE m.user.id=:userId AND m.dateTime >= :startDate AND m.dateTime < :endDate ORDER BY m.dateTime DESC")
+ List getBetweenHalfOpen(@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);
+}
\ No newline at end of file
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..806884a9c90b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java
@@ -0,0 +1,23 @@
+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;
+
+@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);
+
+ User getByEmail(String email);
+
+ @EntityGraph(attributePaths = {"meals", "roles"})
+ @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/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java
new file mode 100644
index 000000000000..dc93b47e5c7b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java
@@ -0,0 +1,58 @@
+package ru.javawebinar.topjava.repository.datajpa;
+
+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;
+
+@Repository
+public class DataJpaMealRepository implements MealRepository {
+
+ private final CrudMealRepository crudMealRepository;
+ private final CrudUserRepository crudUserRepository;
+
+ public DataJpaMealRepository(CrudMealRepository crudMealRepository, CrudUserRepository crudUserRepository) {
+ this.crudMealRepository = crudMealRepository;
+ this.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 crudMealRepository.save(meal);
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ return crudMealRepository.delete(id, userId) != 0;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ return crudMealRepository.findById(id)
+ .filter(meal -> meal.getUser().getId() == userId)
+ .orElse(null);
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return crudMealRepository.getAll(userId);
+ }
+
+ @Override
+ public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ return crudMealRepository.getBetweenHalfOpen(startDateTime, endDateTime, userId);
+ }
+
+ @Override
+ public Meal getWithUser(int id, int userId) {
+ return crudMealRepository.getWithUser(id, userId);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java
new file mode 100644
index 000000000000..608c855e0a05
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java
@@ -0,0 +1,49 @@
+package ru.javawebinar.topjava.repository.datajpa;
+
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import java.util.List;
+
+@Repository
+public class DataJpaUserRepository implements UserRepository {
+ private static final Sort SORT_NAME_EMAIL = Sort.by(Sort.Direction.ASC, "name", "email");
+
+ private final CrudUserRepository crudRepository;
+
+ public DataJpaUserRepository(CrudUserRepository crudRepository) {
+ this.crudRepository = 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.findById(id).orElse(null);
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ return crudRepository.getByEmail(email);
+ }
+
+ @Override
+ public List getAll() {
+ return crudRepository.findAll(SORT_NAME_EMAIL);
+ }
+
+ @Override
+ public User getWithMeals(int id) {
+ return crudRepository.getWithMeals(id);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java
new file mode 100644
index 000000000000..65e1385ad620
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java
@@ -0,0 +1,93 @@
+package ru.javawebinar.topjava.repository.inmemory;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.util.CollectionUtils;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+import ru.javawebinar.topjava.util.MealsUtil;
+import ru.javawebinar.topjava.util.Util;
+
+import java.util.Collection;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.util.Collections;
+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.function.Predicate;
+import java.util.stream.Collectors;
+
+import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.ADMIN_ID;
+import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.USER_ID;
+
+@Repository
+public class InMemoryMealRepository implements MealRepository {
+ private final Map repository = new ConcurrentHashMap<>();
+
+ // Map userId -> (mealId-> meal)
+ private final Map> usersMealsMap = new ConcurrentHashMap<>();
+ private final AtomicInteger counter = new AtomicInteger(0);
+
+ {
+// MealsUtil.meals.forEach(this::save);
+ MealsUtil.meals.forEach(meal -> save(meal, USER_ID));
+ save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510), ADMIN_ID);
+ save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500), ADMIN_ID);
+ }
+
+
+ @Override
+ public Meal save(Meal meal, int userId) {
+ Map meals = usersMealsMap.computeIfAbsent(userId, ConcurrentHashMap::new);
+ if (meal.isNew()) {
+ meal.setId(counter.incrementAndGet());
+ repository.put(meal.getId(), meal);
+ meals.put(meal.getId(), meal);
+ return meal;
+ }
+ // handle case: update, but not present in storage
+ return meals.computeIfPresent(meal.getId(), (id, oldMeal) -> meal);
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ Map meals = usersMealsMap.get(userId);
+ return meals != null && meals.remove(id) != null;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ Map meals = usersMealsMap.get(userId);
+ return meals == null ? null : meals.get(id);
+ }
+
+ @Override
+ public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ return filterByPredicate(userId, meal -> Util.isBetweenHalfOpen(meal.getDateTime(), startDateTime, endDateTime));
+ }
+
+// @Override
+// public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+// return filterByPredicate(userId, meal -> Util.isBetweenHalfOpen(meal.getDateTime(), startDateTime, endDateTime));
+// }
+
+ @Override
+
+ public List getAll(int userId) {
+ return filterByPredicate(userId, meal -> true);
+ }
+
+ private List filterByPredicate(int userId, Predicate filter) {
+ Map meals = usersMealsMap.get(userId);
+ return CollectionUtils.isEmpty(meals) ? Collections.emptyList() :
+ meals.values().stream()
+ .filter(filter)
+ .sorted(Comparator.comparing(Meal::getDateTime).reversed())
+ .collect(Collectors.toList());
+ }
+
+}
+
+
diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java
new file mode 100644
index 000000000000..3565868cc632
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java
@@ -0,0 +1,57 @@
+package ru.javawebinar.topjava.repository.inmemory;
+
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+@Repository
+public class InMemoryUserRepository implements UserRepository {
+
+ public static final int USER_ID = 1;
+ public static final int ADMIN_ID = 2;
+
+ private final Map usersMap = new ConcurrentHashMap<>();
+ private final AtomicInteger counter = new AtomicInteger(0);
+
+ @Override
+ public User save(User user) {
+ if (user.isNew()) {
+ user.setId(counter.incrementAndGet());
+ usersMap.put(user.getId(), user);
+ return user;
+ }
+ return usersMap.computeIfPresent(user.getId(), (id, oldUser) -> user);
+ }
+
+ @Override
+ public boolean delete(int id) {
+ return usersMap.remove(id) != null;
+ }
+
+ @Override
+ public User get(int id) {
+ return usersMap.get(id);
+ }
+
+ @Override
+ public List getAll() {
+ return usersMap.values().stream()
+ .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public User getByEmail(String email) {
+ return usersMap.values().stream()
+ .filter(u -> email.equals(u.getEmail()))
+ .findFirst()
+ .orElse(null);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java
new file mode 100644
index 000000000000..09beed68cd2d
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java
@@ -0,0 +1,98 @@
+package ru.javawebinar.topjava.repository.jdbc;
+
+import org.springframework.beans.factory.annotation.Autowired;
+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 ru.javawebinar.topjava.Profiles;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.MealRepository;
+
+//import org.springframework.jdbc.core.BeanPropertyRowMapper;
+//import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
+import ru.javawebinar.topjava.util.Util;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+@Repository
+public class JdbcMealRepository implements MealRepository {
+
+ private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class);
+
+ private final JdbcTemplate jdbcTemplate;
+
+ private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+
+ private final SimpleJdbcInsert insertMeal;
+
+
+ public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
+ this.insertMeal = new SimpleJdbcInsert(jdbcTemplate)
+ .withTableName("meals")
+ .usingGeneratedKeyColumns("id");
+
+ this.jdbcTemplate = jdbcTemplate;
+ this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
+ }
+
+
+
+ @Override
+ 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", 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
+ 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 getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ return jdbcTemplate.query(
+ "SELECT * FROM meals WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC",
+ ROW_MAPPER, userId, startDateTime, endDateTime);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java
new file mode 100644
index 000000000000..0d4baa50eef2
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java
@@ -0,0 +1,75 @@
+package ru.javawebinar.topjava.repository.jdbc;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.support.DataAccessUtils;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import java.util.List;
+
+@Repository
+public class JdbcUserRepository implements UserRepository {
+
+ private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class);
+
+ private final JdbcTemplate jdbcTemplate;
+
+ private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+
+ private final SimpleJdbcInsert insertUser;
+
+ @Autowired
+ public JdbcUserRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
+ this.insertUser = new SimpleJdbcInsert(jdbcTemplate)
+ .withTableName("users")
+ .usingGeneratedKeyColumns("id");
+
+ this.jdbcTemplate = jdbcTemplate;
+ this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
+ }
+
+ @Override
+ public User save(User user) {
+ BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user);
+
+ if (user.isNew()) {
+ Number newKey = insertUser.executeAndReturnKey(parameterSource);
+ user.setId(newKey.intValue());
+ } else if (namedParameterJdbcTemplate.update("""
+ UPDATE users SET name=:name, email=:email, password=:password,
+ registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id
+ """, parameterSource) == 0) {
+ return null;
+ }
+ return user;
+ }
+
+ @Override
+ 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 DataAccessUtils.singleResult(users);
+ }
+
+ @Override
+ public User getByEmail(String email) {
+// return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email);
+ List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email);
+ return DataAccessUtils.singleResult(users);
+ }
+
+ @Override
+ public List getAll() {
+ return jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java
new file mode 100644
index 000000000000..300a920aea2f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java
@@ -0,0 +1,64 @@
+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;
+
+@Repository
+@Transactional(readOnly = true)
+public class JpaMealRepository implements MealRepository {
+
+ @PersistenceContext
+ private EntityManager em;
+
+ @Override
+ @Transactional
+ public Meal save(Meal meal, int userId) {
+ meal.setUser(em.getReference(User.class, userId));
+ if (meal.isNew()) {
+ em.persist(meal);
+ return meal;
+ } else if (get(meal.id(), userId) == null) {
+ return null;
+ }
+ 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 getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ return em.createNamedQuery(Meal.GET_BETWEEN, Meal.class)
+ .setParameter("userId", userId)
+ .setParameter("startDateTime", startDateTime)
+ .setParameter("endDateTime", endDateTime)
+ .getResultList();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java
new file mode 100644
index 000000000000..9ade13354ac1
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.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;
+
+@Repository
+@Transactional(readOnly = true)
+public class JpaUserRepository 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..5e08c9e5a3dc
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java
@@ -0,0 +1,54 @@
+package ru.javawebinar.topjava.service;
+
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfDayOrMin;
+import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfNextDayOrMax;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId;
+
+@Service
+public class MealService {
+
+ private final MealRepository repository;
+
+ public MealService(MealRepository repository) {
+ this.repository = repository;
+ }
+
+ public Meal get(int id, int userId) {
+ return checkNotFoundWithId(repository.get(id, userId), id);
+ }
+
+ public void delete(int id, int userId) {
+ checkNotFoundWithId(repository.delete(id, userId), id);
+ }
+
+ public List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) {
+ return repository.getBetweenHalfOpen(atStartOfDayOrMin(startDate), atStartOfNextDayOrMax(endDate), userId);
+ }
+
+ public List getAll(int userId) {
+ return repository.getAll(userId);
+ }
+
+ public void update(Meal meal, int userId) {
+ Assert.notNull(meal, "meal must not be null");
+ checkNotFoundWithId(repository.save(meal, userId), meal.id());
+ }
+
+ public Meal create(Meal meal, int userId) {
+ Assert.notNull(meal, "meal must not be null");
+ return repository.save(meal, userId);
+ }
+
+ public Meal getWithUser(int id, int userId) {
+ return checkNotFoundWithId(repository.getWithUser(id, userId), id);
+ }
+}
\ No newline at end of file
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..2fa248762d62
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java
@@ -0,0 +1,58 @@
+package ru.javawebinar.topjava.service;
+
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId;
+
+@Service
+public class UserService {
+
+ private final UserRepository repository;
+
+ public UserService(UserRepository repository) {
+ this.repository = repository;
+ }
+
+ @CacheEvict(value = "users", allEntries = true)
+ public User create(User user) {
+ Assert.notNull(user, "user must not be null");
+ return repository.save(user);
+ }
+
+ @CacheEvict(value = "users", allEntries = true)
+ public void delete(int id) {
+ checkNotFoundWithId(repository.delete(id), id);
+ }
+
+ public User get(int id) {
+ return checkNotFoundWithId(repository.get(id), id);
+ }
+
+ public User getByEmail(String email) {
+ Assert.notNull(email, "email must not be null");
+ return checkNotFound(repository.getByEmail(email), "email=" + email);
+ }
+
+ @Cacheable("users")
+ public List getAll() {
+ return repository.getAll();
+ }
+
+ @CacheEvict(value = "users", allEntries = true)
+ public void update(User user) {
+ Assert.notNull(user, "user must not be null");
+ checkNotFoundWithId(repository.save(user), user.id());
+ }
+
+ public User getWithMeals(int id) {
+ return checkNotFoundWithId(repository.getWithMeals(id), id);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java
new file mode 100644
index 000000000000..c987ac84d47f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java
@@ -0,0 +1,67 @@
+package ru.javawebinar.topjava.to;
+
+import java.time.LocalDateTime;
+
+public class MealTo {
+ private final Integer id;
+
+ private final LocalDateTime dateTime;
+
+ private final String description;
+
+ private final int calories;
+
+// private final AtomicBoolean excess; // filteredByAtomic (or any ref type, e.g. boolean[1])
+// private final Boolean excess; // filteredByReflection
+// private final Supplier excess; // filteredByClosure
+ private boolean excess;
+
+ public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) {
+ this.id = id;
+ this.dateTime = dateTime;
+ this.description = description;
+ this.calories = calories;
+ this.excess = excess;
+ }
+
+// for filteredByClosure
+// public Boolean getExcess() {
+// return excess.get();
+// }
+
+ // for filteredBySetterRecursion
+ public void setExcess(boolean excess) {
+ this.excess = excess;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public LocalDateTime getDateTime() {
+ return dateTime;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public boolean isExcess() {
+ return excess;
+ }
+
+ @Override
+ public String toString() {
+ return "MealTo{" +
+ "id=" + id +
+ ", dateTime=" + dateTime +
+ ", description='" + description + '\'' +
+ ", calories=" + calories +
+ ", excess=" + excess +
+ '}';
+ }
+}
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..bcb8f19d49a7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java
@@ -0,0 +1,76 @@
+package ru.javawebinar.topjava.to;
+
+import ru.javawebinar.topjava.model.Role;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.Set;
+
+import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY;
+
+public class UserTo {
+ private Integer id;
+
+ private String email;
+
+ private String password;
+
+ private boolean enabled = true;
+
+ private Date registered = new Date();
+
+ private Set roles;
+
+ private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY;
+
+ public UserTo(Integer id, String email, String password, boolean enabled, Date registered, Set roles, int caloriesPerDay) {
+ this.id = id;
+ this.email = email;
+ this.password = password;
+ this.enabled = enabled;
+ this.registered = registered;
+ this.roles = roles;
+ this.caloriesPerDay = caloriesPerDay;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public Date getRegistered() {
+ return registered;
+ }
+
+ public Set getRoles() {
+ return roles;
+ }
+
+ public int getCaloriesPerDay() {
+ return caloriesPerDay;
+ }
+
+ @Override
+ public String toString() {
+ return "UserTo{" +
+ "id=" + id +
+ ", email=" + email +
+ ", enabled='" + enabled + '\'' +
+ ", registered=" + registered +
+ ", roles=" + roles +
+ ", caloriesPerDay=" + caloriesPerDay +
+ '}';
+ }
+
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
new file mode 100644
index 000000000000..a4665e2ae0e8
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
@@ -0,0 +1,43 @@
+package ru.javawebinar.topjava.util;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+
+public class DateTimeUtil {
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+
+ // DB doesn't support LocalDate.MIN/MAX
+ private static final LocalDateTime MIN_DATE = LocalDateTime.of(1, 1, 1, 0, 0);
+ private static final LocalDateTime MAX_DATE = LocalDateTime.of(3000, 1, 1, 0, 0);
+
+ private DateTimeUtil() {
+ }
+
+ public static LocalDateTime atStartOfDayOrMin(LocalDate localDate) {
+ return localDate != null ? localDate.atStartOfDay() : MIN_DATE;
+ }
+
+ public static LocalDateTime atStartOfNextDayOrMax(LocalDate localDate) {
+ return localDate != null ? localDate.plus(1, ChronoUnit.DAYS).atStartOfDay() : MAX_DATE;
+ }
+
+ public static String toString(LocalDateTime ldt) {
+ return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER);
+ }
+
+ public static @Nullable
+ LocalDate parseLocalDate(@Nullable String str) {
+ return StringUtils.hasLength(str) ? LocalDate.parse(str) : null;
+ }
+
+ public static @Nullable
+ LocalTime parseLocalTime(@Nullable String str) {
+ return StringUtils.hasLength(str) ? LocalTime.parse(str) : null;
+ }
+}
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..74f437ee18d6
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java
@@ -0,0 +1,382 @@
+package ru.javawebinar.topjava.util;
+
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealTo;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Month;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.function.Predicate;
+import java.util.Collection;
+import java.util.Arrays;
+
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static ru.javawebinar.topjava.util.TimeUtil.isBetweenHalfOpen;
+
+public class MealsUtil {
+
+ public static final int DEFAULT_CALORIES_PER_DAY = 2000;
+
+ public static final List meals = Arrays.asList(
+ new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500),
+ new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000),
+ new Meal(LocalDateTime.of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500),
+ new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100),
+ new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 1000),
+ new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 13, 0), "Обед", 500),
+ new Meal(LocalDateTime.of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 410)
+ );
+
+ public static void main(String[] args) throws InterruptedException {
+
+ final LocalTime startTime = LocalTime.of(7, 0);
+ final LocalTime endTime = LocalTime.of(12, 0);
+
+ List mealsTo = filteredByStreams(meals, startTime, endTime, 2000);
+ mealsTo.forEach(System.out::println);
+
+ System.out.println(filteredByCycles(meals, startTime, endTime, 2000));
+
+// Optional2 recursion
+ System.out.println(filteredByRecursion(meals, startTime, endTime, 2000));
+ System.out.println(filteredBySetterRecursion(meals, startTime, endTime, 2000));
+
+// Optional2 reference type
+// System.out.println(filteredByAtomic(meals, startTime, endTime, 2000)); // or boolean[1]
+// System.out.println(filteredByReflection(meals, startTime, endTime, 2000));
+
+// Optional2 delayed execution
+// System.out.println(filteredByClosure(meals, startTime, endTime, 2000));
+ System.out.println(filteredByExecutor(meals, startTime, endTime, 2000));
+ System.out.println(filteredByLock(meals, startTime, endTime, 2000));
+ System.out.println(filteredByCountDownLatch(meals, startTime, endTime, 2000));
+ System.out.println(filteredByPredicate(meals, startTime, endTime, 2000));
+ System.out.println(filteredByConsumerChain(meals, startTime, endTime, 2000));
+
+// Optional2 streams
+ System.out.println(filteredByFlatMap(meals, startTime, endTime, 2000));
+ System.out.println(filteredByCollector(meals, startTime, endTime, 2000));
+ }
+
+ public static List filteredByStreams(List 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 -> isBetweenHalfOpen(meal.getTime(), startTime, endTime))
+ .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay))
+ .collect(Collectors.toList());
+ }
+
+ public static List filterByPredicate(Collection meals, int caloriesPerDay, Predicate filter) {
+ Map caloriesSumByDate = meals.stream()
+ .collect(
+ Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories))
+
+ );
+
+ return meals.stream()
+ .filter(filter)
+ .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay))
+ .collect(Collectors.toList());
+ }
+
+ private MealsUtil() {
+ }
+
+ public static List getTos(Collection meals, int caloriesPerDay) {
+ return filterByPredicate(meals, caloriesPerDay, meal -> true);
+ }
+
+ public static List getFilteredTos(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) {
+ return filterByPredicate(meals, caloriesPerDay, meal -> Util.isBetweenHalfOpen(meal.getTime(), startTime, endTime));
+ }
+
+ public static List filteredByCycles(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 mealsTo = new ArrayList<>();
+ meals.forEach(meal -> {
+ if (isBetweenHalfOpen(meal.getTime(), startTime, endTime)) {
+ mealsTo.add(createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay));
+ }
+ });
+ return mealsTo;
+ }
+
+ private static List filteredByRecursion(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ ArrayList result = new ArrayList<>();
+ filterWithRecursion(new LinkedList<>(meals), startTime, endTime, caloriesPerDay, new HashMap<>(), result);
+ return result;
+ }
+
+ private static void filterWithRecursion(LinkedList meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay,
+ Map dailyCaloriesMap, List result) {
+ if (meals.isEmpty()) return;
+
+ Meal meal = meals.pop();
+ dailyCaloriesMap.merge(meal.getDate(), meal.getCalories(), Integer::sum);
+ filterWithRecursion(meals, startTime, endTime, caloriesPerDay, dailyCaloriesMap, result);
+ if (isBetweenHalfOpen(meal.getTime(), startTime, endTime)) {
+ result.add(createTo(meal, dailyCaloriesMap.get(meal.getDate()) > caloriesPerDay));
+ }
+ }
+
+ private static List filteredBySetterRecursion(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ class MealNode {
+ private final MealNode prev;
+ private final MealTo mealTo;
+
+ public MealNode(MealTo mealTo, MealNode prev) {
+ this.mealTo = mealTo;
+ this.prev = prev;
+ }
+
+ public void setExcess() {
+ mealTo.setExcess(true);
+ if (prev != null) {
+ prev.setExcess();
+ }
+ }
+ }
+
+ Map caloriesSumByDate = new HashMap<>();
+ Map mealNodeByDate = new HashMap<>();
+ List mealsTo = new ArrayList<>();
+ meals.forEach(meal -> {
+ LocalDate localDate = meal.getDate();
+ boolean excess = caloriesSumByDate.merge(localDate, meal.getCalories(), Integer::sum) > caloriesPerDay;
+ if (TimeUtil.isBetweenHalfOpen(meal.getTime(), startTime, endTime)) {
+ MealTo mealTo = createTo(meal, excess);
+ mealsTo.add(mealTo);
+ if (!excess) {
+ MealNode prevNode = mealNodeByDate.get(localDate);
+ mealNodeByDate.put(localDate, new MealNode(mealTo, prevNode));
+ }
+ }
+ if (excess) {
+ MealNode mealNode = mealNodeByDate.remove(localDate);
+ if (mealNode != null) {
+ // recursive set for all interval day meals
+ mealNode.setExcess();
+ }
+ }
+ });
+ return mealsTo;
+ }
+
+ /*
+ private static List filteredByAtomic(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ Map caloriesSumByDate = new HashMap<>();
+ Map exceededMap = new HashMap<>();
+
+ List mealsTo = new ArrayList<>();
+ meals.forEach(meal -> {
+ AtomicBoolean wrapBoolean = exceededMap.computeIfAbsent(meal.getDate(), date -> new AtomicBoolean());
+ Integer dailyCalories = caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum);
+ if (dailyCalories > caloriesPerDay) {
+ wrapBoolean.set(true);
+ }
+ if (isBetween(meal.getTime(), startTime, endTime)) {
+ mealsTo.add(createTo(meal, wrapBoolean)); // also change createTo and MealTo.excess
+ }
+ });
+ return mealsTo;
+ }
+
+ private static List filteredByReflection(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) throws NoSuchFieldException, IllegalAccessException {
+ Map caloriesSumByDate = new HashMap<>();
+ Map exceededMap = new HashMap<>();
+ Field field = Boolean.class.getDeclaredField("value");
+ field.setAccessible(true);
+
+ List mealsTo = new ArrayList<>();
+ for (Meal meal : meals) {
+ Boolean mutableBoolean = exceededMap.computeIfAbsent(meal.getDate(), date -> new Boolean(false));
+ Integer dailyCalories = caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum);
+ if (dailyCalories > caloriesPerDay) {
+ field.setBoolean(mutableBoolean, true);
+ }
+ if (isBetweenHalfOpen(meal.getTime(), startTime, endTime)) {
+ mealsTo.add(createTo(meal, mutableBoolean)); // also change createTo and MealTo.excess
+ }
+ }
+ return mealsTo;
+ }
+
+ private static List filteredByClosure(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ final Map caloriesSumByDate = new HashMap<>();
+ List mealsTo = new ArrayList<>();
+ mealList.forEach(meal -> {
+ caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum);
+ if (isBetween(meal.getTime(), startTime, endTime)) {
+ mealsTo.add(createTo(meal, () -> (caloriesSumByDate.get(meal.getDate()) > caloriesPerDay))); // also change createTo and MealTo.excess
+ }
+ }
+ );
+ return mealsTo;
+ }
+ */
+
+ private static List filteredByExecutor(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) throws InterruptedException {
+ Map caloriesSumByDate = new HashMap<>();
+ List> tasks = new ArrayList<>();
+ final List mealsTo = Collections.synchronizedList(new ArrayList<>());
+
+ meals.forEach(meal -> {
+ caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum);
+ if (isBetweenHalfOpen(meal.getTime(), startTime, endTime)) {
+ tasks.add(() -> {
+ mealsTo.add(createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay));
+ return null;
+ });
+ }
+ });
+ ExecutorService executorService = Executors.newCachedThreadPool();
+ executorService.invokeAll(tasks);
+ executorService.shutdown();
+ return mealsTo;
+ }
+
+ public static List filteredByLock(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) throws InterruptedException {
+ Map caloriesSumByDate = new HashMap<>();
+ List mealsTo = new ArrayList<>();
+ ExecutorService executor = Executors.newCachedThreadPool();
+ ReentrantLock lock = new ReentrantLock();
+ lock.lock();
+ for (Meal meal : meals) {
+ caloriesSumByDate.merge(meal.getDateTime().toLocalDate(), meal.getCalories(), Integer::sum);
+ if (isBetweenHalfOpen(meal.getDateTime().toLocalTime(), startTime, endTime))
+ executor.submit(() -> {
+ lock.lock();
+ mealsTo.add(createTo(meal, caloriesSumByDate.get(meal.getDateTime().toLocalDate()) > caloriesPerDay));
+ lock.unlock();
+ });
+ }
+ lock.unlock();
+ executor.shutdown();
+ executor.awaitTermination(5, TimeUnit.SECONDS);
+ return mealsTo;
+ }
+
+ private static List filteredByCountDownLatch(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) throws InterruptedException {
+ Map caloriesSumByDate = new HashMap<>();
+ List mealsTo = Collections.synchronizedList(new ArrayList<>());
+ CountDownLatch latchCycles = new CountDownLatch(meals.size());
+ CountDownLatch latchTasks = new CountDownLatch(meals.size());
+ for (Meal meal : meals) {
+ caloriesSumByDate.merge(meal.getDateTime().toLocalDate(), meal.getCalories(), Integer::sum);
+ if (isBetweenHalfOpen(meal.getDateTime().toLocalTime(), startTime, endTime)) {
+ new Thread(() -> {
+ try {
+ latchCycles.await();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ mealsTo.add(createTo(meal, caloriesSumByDate.get(meal.getDateTime().toLocalDate()) > caloriesPerDay));
+ latchTasks.countDown();
+ }).start();
+ } else {
+ latchTasks.countDown();
+ }
+ latchCycles.countDown();
+ }
+ latchTasks.await();
+ return mealsTo;
+ }
+
+ public static List filteredByPredicate(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ Map caloriesSumByDate = new HashMap<>();
+ List mealsTo = new ArrayList<>();
+
+ Predicate predicate = b -> true;
+ for (Meal meal : meals) {
+ caloriesSumByDate.merge(meal.getDateTime().toLocalDate(), meal.getCalories(), Integer::sum);
+ if (TimeUtil.isBetweenHalfOpen(meal.getDateTime().toLocalTime(), startTime, endTime)) {
+ predicate = predicate.and(b -> mealsTo.add(createTo(meal, caloriesSumByDate.get(meal.getDateTime().toLocalDate()) > caloriesPerDay)));
+ }
+ }
+ predicate.test(true);
+ return mealsTo;
+ }
+
+ public static List filteredByConsumerChain(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ Map caloriesPerDays = new HashMap<>();
+ List result = new ArrayList<>();
+ Consumer consumer = dummy -> {};
+
+ for (Meal meal : meals) {
+ caloriesPerDays.merge(meal.getDate(), meal.getCalories(), Integer::sum);
+ if (TimeUtil.isBetweenHalfOpen(meal.getTime(), startTime, endTime)) {
+ consumer = consumer.andThen(dummy -> result.add(createTo(meal, caloriesPerDays.get(meal.getDateTime().toLocalDate()) > caloriesPerDay)));
+ }
+ }
+ consumer.accept(null);
+ return result;
+ }
+
+ private static List filteredByFlatMap(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ Collection> list = meals.stream()
+ .collect(Collectors.groupingBy(Meal::getDate)).values();
+
+ return list.stream()
+ .flatMap(dayMeals -> {
+ boolean excess = dayMeals.stream().mapToInt(Meal::getCalories).sum() > caloriesPerDay;
+ return dayMeals.stream().filter(meal ->
+ isBetweenHalfOpen(meal.getTime(), startTime, endTime))
+ .map(meal -> createTo(meal, excess));
+ }).collect(toList());
+ }
+
+ private static List filteredByCollector(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) {
+ final class Aggregate {
+ private final List dailyMeals = new ArrayList<>();
+ private int dailySumOfCalories;
+
+ private void accumulate(Meal meal) {
+ dailySumOfCalories += meal.getCalories();
+ if (isBetweenHalfOpen(meal.getTime(), startTime, endTime)) {
+ dailyMeals.add(meal);
+ }
+ }
+
+ // never invoked if the upstream is sequential
+ private Aggregate combine(Aggregate that) {
+ this.dailySumOfCalories += that.dailySumOfCalories;
+ this.dailyMeals.addAll(that.dailyMeals);
+ return this;
+ }
+
+ private Stream finisher() {
+ final boolean excess = dailySumOfCalories > caloriesPerDay;
+ return dailyMeals.stream().map(meal -> createTo(meal, excess));
+ }
+ }
+
+ Collection> values = meals.stream()
+ .collect(Collectors.groupingBy(Meal::getDate,
+ Collector.of(Aggregate::new, Aggregate::accumulate, Aggregate::combine, Aggregate::finisher))
+ ).values();
+
+ return values.stream().flatMap(identity()).collect(toList());
+ }
+
+ private static MealTo createTo(Meal meal, boolean excess) {
+ return new MealTo(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess);
+ }
+}
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..7f93c793ef33
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java
@@ -0,0 +1,18 @@
+package ru.javawebinar.topjava.util;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+public class TimeUtil {
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+
+ public static boolean isBetweenHalfOpen(LocalTime lt, LocalTime startTime, LocalTime endTime) {
+ return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) < 0;
+ }
+
+ public static String toString(LocalDateTime ldt) {
+ return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER);
+ }
+}
+
diff --git a/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java b/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java
new file mode 100644
index 000000000000..61db1dec031c
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java
@@ -0,0 +1,39 @@
+package ru.javawebinar.topjava.util;
+
+import ru.javawebinar.topjava.model.*;
+import ru.javawebinar.topjava.to.UserTo;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+public class UsersUtil {
+ public static final List users = Arrays.asList(
+ new User(0, "admin", "admin@anklav.ru", "1", Role.ADMIN),
+ new User(1, "Ivan", "Ivan@anklav.ru", "1", Role.USER),
+ new User(2, "Anton", "Anton@anklav.ru", "1", Role.USER),
+ new User(3, "Sasha", "Sasha@anklav.ru", "1", Role.USER)
+ );
+
+ public static List getTos(Collection users) {
+ return filterByPredicate(users, user -> true);
+ }
+
+ public static List getFilteredTos(Collection users) {
+ return filterByPredicate(users, user -> true);
+ }
+
+ public static List filterByPredicate(Collection users, Predicate filter) {
+
+ return users.stream()
+ .filter(filter)
+ .map(user -> createTo(user))
+ .collect(Collectors.toList());
+ }
+
+ private static UserTo createTo(User user) {
+ return new UserTo(user.getId(), user.getEmail(), user.getPassword(), user.isEnabled(), user.getRegistered(), user.getRoles(), user.getCaloriesPerDay());
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java
new file mode 100644
index 000000000000..0860f5c6bf3c
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/Util.java
@@ -0,0 +1,12 @@
+package ru.javawebinar.topjava.util;
+
+import org.springframework.lang.Nullable;
+
+public class Util {
+ private Util() {
+ }
+
+ public static > boolean isBetweenHalfOpen(T value, @Nullable T start, @Nullable T end) {
+ return (start == null || value.compareTo(start) >= 0) && (end == null || value.compareTo(end) < 0);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java
new file mode 100644
index 000000000000..0f35f22a05d3
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java
@@ -0,0 +1,45 @@
+package ru.javawebinar.topjava.util;
+
+
+import ru.javawebinar.topjava.model.AbstractBaseEntity;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+public class ValidationUtil {
+ private ValidationUtil() {
+ }
+
+ public static T checkNotFoundWithId(T object, int id) {
+ checkNotFoundWithId(object != null, id);
+ return object;
+ }
+
+ public static void checkNotFoundWithId(boolean found, int id) {
+ checkNotFound(found, "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);
+ }
+ }
+
+ public static void checkNew(AbstractBaseEntity entity) {
+ if (!entity.isNew()) {
+ throw new IllegalArgumentException(entity + " must be new (id=null)");
+ }
+ }
+
+ public static void assureIdConsistent(AbstractBaseEntity entity, int id) {
+// conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473)
+ if (entity.isNew()) {
+ entity.setId(id);
+ } else if (entity.id() != id) {
+ throw new IllegalArgumentException(entity + " must be with id=" + id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
new file mode 100644
index 000000000000..f1e9b0e46376
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
@@ -0,0 +1,7 @@
+package ru.javawebinar.topjava.util.exception;
+
+public class NotFoundException extends RuntimeException {
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java
new file mode 100644
index 000000000000..defeb1d7005a
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java
@@ -0,0 +1,92 @@
+package ru.javawebinar.topjava.web;
+
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.springframework.util.StringUtils;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.web.meal.MealRestController;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Objects;
+
+import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate;
+import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime;
+
+public class MealServlet extends HttpServlet {
+
+ private ConfigurableApplicationContext springContext;
+ private MealRestController mealController;
+
+ @Override
+ public void init() {
+ springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/spring-db.xml");
+ mealController = springContext.getBean(MealRestController.class);
+ }
+
+ @Override
+ public void destroy() {
+ springContext.close();
+ super.destroy();
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ request.setCharacterEncoding("UTF-8");
+ Meal meal = new Meal(
+ LocalDateTime.parse(request.getParameter("dateTime")),
+ request.getParameter("description"),
+ Integer.parseInt(request.getParameter("calories")));
+
+ if (StringUtils.hasLength(request.getParameter("id"))) {
+ mealController.update(meal, getId(request));
+ } else {
+ mealController.create(meal);
+ }
+ response.sendRedirect("meals");
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String action = request.getParameter("action");
+
+ switch (action == null ? "all" : action) {
+ case "delete" -> {
+ int id = getId(request);
+ mealController.delete(id);
+ response.sendRedirect("meals");
+ }
+ case "create", "update" -> {
+ final Meal meal = "create".equals(action) ?
+ new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) :
+ mealController.get(getId(request));
+ request.setAttribute("meal", meal);
+ request.getRequestDispatcher("/mealForm.jsp").forward(request, response);
+ }
+ case "filter" -> {
+ LocalDate startDate = parseLocalDate(request.getParameter("startDate"));
+ LocalDate endDate = parseLocalDate(request.getParameter("endDate"));
+ LocalTime startTime = parseLocalTime(request.getParameter("startTime"));
+ LocalTime endTime = parseLocalTime(request.getParameter("endTime"));
+ request.setAttribute("meals", mealController.getBetween(startDate, startTime, endDate, endTime));
+ request.getRequestDispatcher("/meals.jsp").forward(request, response);
+ }
+ default -> {
+ request.setAttribute("meals", mealController.getAll());
+ request.getRequestDispatcher("/meals.jsp").forward(request, response);
+ }
+ }
+ }
+
+ private int getId(HttpServletRequest request) {
+ String paramId = Objects.requireNonNull(request.getParameter("id"));
+ return Integer.parseInt(paramId);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java
new file mode 100644
index 000000000000..4bad5863e3c6
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java
@@ -0,0 +1,25 @@
+package ru.javawebinar.topjava.web;
+
+import ru.javawebinar.topjava.model.AbstractBaseEntity;
+
+import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY;
+
+public class SecurityUtil {
+
+ private static int id = AbstractBaseEntity.START_SEQ;
+
+ private SecurityUtil() {
+ }
+
+ public static int authUserId() {
+ return id;
+ }
+
+ public static void setAuthUserId(int id) {
+ SecurityUtil.id = id;
+ }
+
+ public static int authUserCaloriesPerDay() {
+ return DEFAULT_CALORIES_PER_DAY;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java
new file mode 100644
index 000000000000..226023400c70
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java
@@ -0,0 +1,28 @@
+package ru.javawebinar.topjava.web;
+
+import org.slf4j.Logger;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+public class UserServlet extends HttpServlet {
+ private static final Logger log = getLogger(UserServlet.class);
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ int userId = Integer.parseInt(request.getParameter("userId"));
+ SecurityUtil.setAuthUserId(userId);
+ response.sendRedirect("meals");
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ log.debug("forward to users");
+ request.getRequestDispatcher("/users.jsp").forward(request, response);
+ }
+}
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..2f26c81fafeb
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
@@ -0,0 +1,76 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Controller;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.service.MealService;
+import ru.javawebinar.topjava.to.MealTo;
+import ru.javawebinar.topjava.util.MealsUtil;
+import ru.javawebinar.topjava.web.SecurityUtil;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNew;
+
+@Controller
+public class MealRestController {
+ private static final Logger log = LoggerFactory.getLogger(MealRestController.class);
+
+ private final MealService service;
+
+ public MealRestController(MealService service) {
+ this.service = service;
+ }
+
+ public Meal get(int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("get meal {} for user {}", id, userId);
+ return service.get(id, userId);
+ }
+
+ public void delete(int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("delete meal {} for user {}", id, userId);
+ service.delete(id, userId);
+ }
+
+ public List getAll() {
+ int userId = SecurityUtil.authUserId();
+ log.info("getAll for user {}", userId);
+ return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay());
+ }
+
+ public Meal create(Meal meal) {
+ int userId = SecurityUtil.authUserId();
+ log.info("create {} for user {}", meal, userId);
+ checkNew(meal);
+ return service.create(meal, userId);
+ }
+
+ public void update(Meal meal, int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("update {} for user {}", meal, userId);
+ assureIdConsistent(meal, id);
+ service.update(meal, userId);
+ }
+
+ /**
+ * Filter separately
+ *
by date
+ *
by time for every date
+ *
+ */
+ public List getBetween(@Nullable LocalDate startDate, @Nullable LocalTime startTime,
+ @Nullable LocalDate endDate, @Nullable LocalTime endTime) {
+ int userId = SecurityUtil.authUserId();
+ log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId);
+
+ List mealsDateFiltered = service.getBetweenInclusive(startDate, endDate, userId);
+ return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
new file mode 100644
index 000000000000..0000f1c1e02f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -0,0 +1,51 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNew;
+
+public abstract class AbstractUserController {
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ @Autowired
+ private UserService service;
+
+ public List getAll() {
+ log.info("getAll");
+ return service.getAll();
+ }
+
+ public User get(int id) {
+ log.info("get {}", id);
+ return service.get(id);
+ }
+
+ public User create(User user) {
+ log.info("create {}", user);
+ checkNew(user);
+ return service.create(user);
+ }
+
+ public void delete(int id) {
+ log.info("delete {}", id);
+ service.delete(id);
+ }
+
+ public void update(User user, int id) {
+ log.info("update {} with id={}", user, id);
+ assureIdConsistent(user, id);
+ service.update(user);
+ }
+
+ public User getByMail(String email) {
+ log.info("getByEmail {}", email);
+ return service.getByEmail(email);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
new file mode 100644
index 000000000000..b37a8ed6c8a5
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
@@ -0,0 +1,40 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.stereotype.Controller;
+import ru.javawebinar.topjava.model.User;
+
+import java.util.List;
+
+@Controller
+public class AdminRestController extends AbstractUserController {
+
+ @Override
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ public User get(int id) {
+ return super.get(id);
+ }
+
+ @Override
+ public User create(User user) {
+ return super.create(user);
+ }
+
+ @Override
+ public void delete(int id) {
+ super.delete(id);
+ }
+
+ @Override
+ public void update(User user, int id) {
+ super.update(user, id);
+ }
+
+ @Override
+ public User getByMail(String email) {
+ return super.getByMail(email);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
new file mode 100644
index 000000000000..7d3702c31c46
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,22 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.stereotype.Controller;
+import ru.javawebinar.topjava.model.User;
+
+import static ru.javawebinar.topjava.web.SecurityUtil.authUserId;
+
+@Controller
+public class ProfileRestController extends AbstractUserController {
+
+ public User get() {
+ return super.get(authUserId());
+ }
+
+ public void delete() {
+ super.delete(authUserId());
+ }
+
+ public void update(User user) {
+ super.update(user, authUserId());
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml
new file mode 100644
index 000000000000..05589f71f06e
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+ 5
+
+ 5000
+
+
+
+
+
+
+ 1
+
+
+
+
diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties
new file mode 100644
index 000000000000..17c03ef4ebda
--- /dev/null
+++ b/src/main/resources/db/hsqldb.properties
@@ -0,0 +1,11 @@
+#database.url=jdbc:hsqldb:file:D:/temp/topjava
+
+database.url=jdbc:hsqldb:mem:topjava
+database.username=sa
+database.password=
+
+database.init=true
+jdbc.initLocation=classpath:db/initDB_hsql.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql
new file mode 100644
index 000000000000..b444c29cfe98
--- /dev/null
+++ b/src/main/resources/db/initDB.sql
@@ -0,0 +1,39 @@
+DROP TABLE IF EXISTS meals;
+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 WITH 100000;
+
+CREATE TABLE users
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ name VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ password VARCHAR NOT NULL,
+ registered TIMESTAMP DEFAULT now() NOT NULL,
+ enabled BOOL DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
+
+CREATE TABLE user_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);
+
diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql
new file mode 100644
index 000000000000..37f2da1bf3d8
--- /dev/null
+++ b/src/main/resources/db/initDB_hsql.sql
@@ -0,0 +1,39 @@
+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) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ registered TIMESTAMP DEFAULT now() NOT NULL,
+ enabled BOOLEAN DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx
+ ON USERS (email);
+
+CREATE TABLE user_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..4aa90f0095e2
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,27 @@
+DELETE FROM meals;
+DELETE FROM user_roles;
+DELETE FROM meals;
+DELETE FROM users;
+
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+INSERT INTO users (name, email, password)
+VALUES ('User', 'user@yandex.ru', 'password'),
+ ('Admin', 'admin@gmail.com', 'admin');
+
+INSERT INTO user_roles (role, user_id)
+VALUES ('USER', 100000),
+ ('ADMIN', 100001);
+
+INSERT INTO meals (date_time, description, calories, user_id)
+VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000),
+ ('2020-01-30 13:00:00', 'Обед', 1000, 100000),
+ ('2020-01-30 20:00:00', 'Ужин', 500, 100000),
+ ('2020-01-31 0:00:00', 'Еда на граничное значение', 100, 100000),
+ ('2020-01-31 10:00:00', 'Завтрак', 500, 100000),
+ ('2020-01-31 13:00:00', 'Обед', 1000, 100000),
+ ('2020-01-31 20:00:00', 'Ужин', 510, 100000),
+ ('2020-01-31 14:00:00', 'Админ ланч', 510, 100001),
+ ('2020-01-31 21:00:00', 'Админ ужин', 1500, 100001);
+
+
diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties
new file mode 100644
index 000000000000..63817b2c85bf
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,13 @@
+#database.url=jdbc:postgresql://ec2-54-247-74-197.eu-west-1.compute.amazonaws.com:5432/de4fjsqhdvl7ld?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl.NonValidatingFactory
+#database.username=anbxkjtzukqacj
+#database.password=da1f25b2a38784fb0d46858e5b8fc168e08c9e1e9c72faea5bbac9c0e1f9c24f
+
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=user
+database.password=password
+
+database.init=true
+jdbc.initLocation=classpath:db/initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+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..12a4f63c6c15
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ ${TOPJAVA_ROOT}/log/topjava.log
+
+
+ UTF-8
+ %date %-5level %logger{50}.%M:%L - %msg%n
+
+
+
+
+
+ UTF-8
+ %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml
new file mode 100644
index 000000000000..d6c643e972e1
--- /dev/null
+++ b/src/main/resources/spring/spring-app.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-cache.xml b/src/main/resources/spring/spring-cache.xml
new file mode 100644
index 000000000000..73325fee065f
--- /dev/null
+++ b/src/main/resources/spring/spring-cache.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml
new file mode 100644
index 000000000000..47502d38674f
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld
new file mode 100644
index 000000000000..d138fecdbfb5
--- /dev/null
+++ b/src/main/webapp/WEB-INF/tld/functions.tld
@@ -0,0 +1,16 @@
+
+
+
+ 1.0
+ functions
+ http://topjava.javawebinar.ru/functions
+
+
+ formatDateTime
+ ru.javawebinar.topjava.util.DateTimeUtil
+ java.lang.String toString(java.time.LocalDateTime)
+
+
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000000..bd98d3bf3f6a
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,29 @@
+
+
+ Topjava
+
+
+ userServlet
+ ru.javawebinar.topjava.web.UserServlet
+ 0
+
+
+ userServlet
+ /users
+
+
+
+ mealServlet
+ ru.javawebinar.topjava.web.MealServlet
+ 0
+
+
+ mealServlet
+ /meals
+
+
+
diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css
new file mode 100644
index 000000000000..26a14ce43e6d
--- /dev/null
+++ b/src/main/webapp/css/style.css
@@ -0,0 +1,24 @@
+dl {
+ background: none repeat scroll 0 0 #FAFAFA;
+ margin: 8px 0;
+ padding: 0;
+}
+
+dt {
+ display: inline-block;
+ width: 170px;
+}
+
+dd {
+ display: inline-block;
+ margin-left: 8px;
+ vertical-align: top;
+}
+
+tr[data-mealExcess="false"] {
+ color: green;
+}
+
+tr[data-mealExcess="true"] {
+ color: red;
+}
diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html
new file mode 100644
index 000000000000..1e2ac3b901d9
--- /dev/null
+++ b/src/main/webapp/index.html
@@ -0,0 +1,18 @@
+
+
+
+ Java Enterprise (Topjava)
+
+
+