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/.gitignore b/.gitignore
index 73e6d9e3dd11..92fe072b939f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ log
*.patch
+/patch/
+/lib/
diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml
new file mode 100644
index 000000000000..f1cb3335106b
--- /dev/null
+++ b/config/Topjava-soapui-project.xml
@@ -0,0 +1,609 @@
+
+
+
+
+
+
+
+ http://localhost:8080
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 500 401
+
+ html
+
+
+ application/json
+ 200
+
+ ns:Response
+
+
+ application/json;charset=UTF-8
+ 200
+
+ Response
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/admin/users
+
+ user@yandex.ru
+ password
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+ application/json
+
+
+
+ text/html;charset=utf-8
+ 500
+
+ html
+
+
+ application/json
+ 201
+
+ user:Response
+
+
+ application/json;charset=UTF-8
+ 201
+
+ user:Response
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+ {"name": "New2",
+ "email": "new2@yandex.ru",
+ "password": "passwordNew",
+ "roles": ["USER"]
+ }
+
+ http://localhost/topjava/rest/admin/users
+
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 500
+
+ html
+
+
+ application/json
+ 200
+
+ ns:Response
+
+
+ application/json;charset=UTF-8
+ 200
+
+ ns:Response
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/admin/users/100000
+
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 405 500
+
+ html
+
+
+ application/json
+
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+ {"name": "UserUpdated",
+ "email": "user@yandex.ru",
+ "password": "passwordNew",
+ "roles": ["USER"]
+ }
+
+ http://localhost/topjava/rest/admin/users/100000
+
+ Basic
+ Basic
+ Global HTTP Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/html;charset=utf-8
+ 500
+
+ html
+
+
+ application/json
+ 200
+
+ ns:Response
+
+
+ application/json;charset=UTF-8
+ 200
+
+ prof:Response
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile
+
+ Basic
+ Basic
+ Global HTTP Settings
+
+
+
+
+
+
+
+
+
+
+ application/json
+
+
+
+ text/html;charset=utf-8
+ 500 405
+
+ html
+
+
+ application/json
+ 201
+
+ user:Response
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+ {"name": "New777",
+ "email": "new777@yandex.ru",
+ "password": "passwordNew",
+ "roles": ["USER"]
+ }
+
+ http://localhost/topjava/rest/profile
+
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+ 200
+
+ data
+
+
+ text/html;charset=utf-8
+ 500
+
+ html
+
+
+ application/json
+
+
+
+
+ 200
+
+ data
+
+
+
+ 204
+
+ data
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile
+
+ No Authorization
+ Basic
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+ data
+
+
+ application/json;charset=UTF-8
+ 200
+
+ Response
+
+
+
+ <xml-fragment/>
+
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile/meals
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+ application/json
+
+
+
+ application/json;charset=UTF-8
+ 201
+
+ meal:Response
+
+
+
+ <xml-fragment/>
+
+
+ http://localhost:8080
+ {
+ "dateTime": "2020-02-01T10:00",
+ "description": "Новый завтрак",
+ "calories": 777
+ }
+
+ http://localhost/topjava/rest/profile/meals
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ application/json;charset=UTF-8
+ 200
+
+ ns:Response
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile/meals/100002
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+
+ 200
+
+ data
+
+
+ application/json
+
+
+
+
+ 200
+
+ data
+
+
+
+ 200
+
+ data
+
+
+
+ <xml-fragment/>
+
+ UTF-8
+ http://localhost:8080
+ {
+ "id": 100002,
+ "dateTime": "2020-01-30T10:00",
+ "description": "Обновленный завтрак",
+ "calories": 500
+ }
+
+ http://localhost/topjava/rest/profile/meals/100002
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+
+
+ startDate
+
+ QUERY
+
+
+
+
+ startTime
+
+ QUERY
+
+
+
+
+ endDate
+
+ QUERY
+
+
+
+
+ endTime
+
+ QUERY
+
+
+
+
+
+
+
+
+ application/json;charset=UTF-8
+ 200
+
+ Response
+
+
+
+ <xml-fragment/>
+
+ http://localhost:8080
+
+ http://localhost/topjava/rest/profile/meals/filter
+
+ No Authorization
+
+
+
+
+
+
+
+
+
+
+ startDate
+ startTime
+ endDate
+ endTime
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config/curl.md b/config/curl.md
new file mode 100644
index 000000000000..8480dbc7d5df
--- /dev/null
+++ b/config/curl.md
@@ -0,0 +1,29 @@
+### curl samples (application deployed at application context `topjava`).
+> For windows use `Git Bash`
+
+#### get All Users
+`curl -s http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin`
+
+#### get Users 100001
+`curl -s http://localhost:8080/topjava/rest/admin/users/100001 --user admin@gmail.com:admin`
+
+#### get All Meals
+`curl -s http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password`
+
+#### get Meals 100003
+`curl -s http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password`
+
+#### filter Meals
+`curl -s "http://localhost:8080/topjava/rest/profile/meals/filter?startDate=2020-01-30&startTime=07:00:00&endDate=2020-01-31&endTime=11:00:00" --user user@yandex.ru:password`
+
+#### get Meals not found
+`curl -s -v http://localhost:8080/topjava/rest/profile/meals/100008 --user user@yandex.ru:password`
+
+#### delete Meals
+`curl -s -X DELETE http://localhost:8080/topjava/rest/profile/meals/100002 --user user@yandex.ru:password`
+
+#### create Meals
+`curl -s -X POST -d '{"dateTime":"2020-02-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password`
+
+#### update Meals
+`curl -s -X PUT -d '{"dateTime":"2020-01-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password`
\ No newline at end of file
diff --git a/config/messages/app.properties b/config/messages/app.properties
new file mode 100644
index 000000000000..f34c18257b9e
--- /dev/null
+++ b/config/messages/app.properties
@@ -0,0 +1,41 @@
+app.title=Calories management
+app.stackTitle=Application stack:
+app.description=Java Enterprise project with registration/authorization and role-based access rights (USER, ADMIN). \
+Admin could create/edit/delete users, users - manage your profile and data (meals) via UI (AJAX) and REST with basic authorization. \
+Meals could be filtered by date and time. Meal record color depends on daily calories sum exceeding "Daily calorie limit" (editable user's profile paramets). \
+All REST interface covered with JUnit tests by Spring MVC Test and Spring Security Test.
+app.footer=Spring 5/JPA Enterprise (Topjava) internship application
+app.login=Login as
+
+user.title=Users
+user.edit=Edit user
+user.add=Add user
+user.name=Name
+user.email=Email
+user.roles=Roles
+user.active=Active
+user.registered=Registered
+user.password=Password
+
+meal.title=Meals
+meal.edit=Edit meal
+meal.add=Add meal
+meal.filter=Filter
+meal.startDate=From date (inclusive)
+meal.endDate=To date (inclusive)
+meal.startTime=From time (inclusive)
+meal.endTime=To time (exclusive)
+meal.description=Description
+meal.dateTime=Date/Time
+meal.calories=Calories
+
+common.add=Add
+common.select=Select
+common.deleted=Record deleted
+common.saved=Record saved
+common.enabled=Record enabled
+common.disabled=Record disabled
+common.errorStatus=Error status
+common.confirm=Are you sure?
+common.save=Save
+common.cancel=Cancel
\ No newline at end of file
diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties
new file mode 100644
index 000000000000..57e5b9415d95
--- /dev/null
+++ b/config/messages/app_ru.properties
@@ -0,0 +1,41 @@
+app.title=\u041F\u043E\u0434\u0441\u0447\u0435\u0442 \u043A\u0430\u043B\u043E\u0440\u0438\u0439
+app.stackTitle=\u0421\u0442\u0435\u043A \u0442\u0435\u0445\u043D\u043E\u043B\u043E\u0433\u0438\u0439:
+app.description=Java Enterprise \u043F\u0440\u043E\u0435\u043A\u0442 \u0441 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0435\u0439/\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0438 \u043F\u0440\u0430\u0432\u0430\u043C\u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0440\u043E\u043B\u0435\u0439 (USER, ADMIN). \
+\u0410\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0436\u0435\u0442 \u0441\u043E\u0437\u0434\u0430\u0432\u0430\u0442\u044C/\u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C/\u0443\u0434\u0430\u043B\u044F\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439, \u0430 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 - \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C \u0441\u0432\u043E\u0438\u043C \u043F\u0440\u043E\u0444\u0438\u043B\u0435\u043C \u0438 \u0434\u0430\u043D\u043D\u044B\u043C\u0438 (\u0435\u0434\u043E\u0439) \u0447\u0435\u0440\u0435\u0437 UI (\u043F\u043E AJAX) \u0438 \u043F\u043E REST \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u0441 \u0431\u0430\u0437\u043E\u0432\u043E\u0439 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439. \
+\u0412\u043E\u0437\u043C\u043E\u0436\u043D\u0430 \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u044F \u0435\u0434\u044B \u043F\u043E \u0434\u0430\u0442\u0430\u043C \u0438 \u0432\u0440\u0435\u043C\u0435\u043D\u0438. \u0426\u0432\u0435\u0442 \u0437\u0430\u043F\u0438\u0441\u0438 \u0442\u0430\u0431\u043B\u0438\u0446\u044B \u0435\u0434\u044B \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043E\u0442 \u0442\u043E\u0433\u043E, \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 \u043B\u0438 \u0441\u0443\u043C\u043C\u0430 \u043A\u0430\u043B\u043E\u0440\u0438\u0439 \u0437\u0430 \u0434\u0435\u043D\u044C \u043D\u043E\u0440\u043C\u0443 (\u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0435\u043C\u044B\u0439 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432 \u043F\u0440\u043E\u0444\u0438\u043B\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F). \
+\u0412\u0435\u0441\u044C REST \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043F\u043E\u043A\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F JUnit \u0442\u0435\u0441\u0442\u0430\u043C\u0438, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u044F Spring MVC Test \u0438 Spring Security Test.
+app.footer=\u041F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0441\u0442\u0430\u0436\u0438\u0440\u043E\u0432\u043A\u0438 Spring 5/JPA Enterprise (Topjava)
+app.login=\u0417\u0430\u0439\u0442\u0438 \u043A\u0430\u043A
+
+user.title=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438
+user.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
+user.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
+user.name=\u0418\u043C\u044F
+user.email=\u041F\u043E\u0447\u0442\u0430
+user.roles=\u0420\u043E\u043B\u0438
+user.active=\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0439
+user.registered=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D
+user.password=\u041F\u0430\u0440\u043E\u043B\u044C
+
+meal.title=\u041C\u043E\u044F \u0435\u0434\u0430
+meal.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0435\u0434\u0443
+meal.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0435\u0434\u0443
+meal.filter=\u041E\u0442\u0444\u0438\u043B\u044C\u0442\u0440\u043E\u0432\u0430\u0442\u044C
+meal.startDate=\u041E\u0442 \u0434\u0430\u0442\u044B (\u0432\u043A\u043B\u044E\u0447\u0430\u044F)
+meal.endDate=\u0414\u043E \u0434\u0430\u0442\u044B (\u0432\u043A\u043B\u044E\u0447\u0430\u044F)
+meal.startTime=\u041E\u0442 \u0432\u0440\u0435\u043C\u0435\u043D\u0438 (\u0432\u043A\u043B\u044E\u0447\u0430\u044F)
+meal.endTime=\u0414\u043E \u0432\u0440\u0435\u043C\u0435\u043D\u0438 (\u0438\u0441\u043A\u043B\u044E\u0447\u0430\u044F)
+meal.description=\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435
+meal.dateTime=\u0414\u0430\u0442\u0430/\u0412\u0440\u0435\u043C\u044F
+meal.calories=\u041A\u0430\u043B\u043E\u0440\u0438\u0438
+
+common.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C
+common.select=\u0412\u044B\u0431\u0440\u0430\u0442\u044C
+common.deleted=\u0417\u0430\u043F\u0438\u0441\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u0430
+common.saved=\u0417\u0430\u043F\u0438\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430
+common.enabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430
+common.disabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0434\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430
+common.errorStatus=\u0421\u0442\u0430\u0442\u0443\u0441 \u043E\u0448\u0438\u0431\u043A\u0438
+common.confirm=\u0412\u044B \u0443\u0432\u0435\u0440\u0435\u043D\u044B?
+common.save=\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C
+common.cancel=\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C
\ No newline at end of file
diff --git a/doc/lesson07.md b/doc/lesson07.md
deleted file mode 100644
index b958f9d2a74a..000000000000
--- a/doc/lesson07.md
+++ /dev/null
@@ -1,566 +0,0 @@
-# [Онлайн стажировка Spring 5/JPA Enterprise (TopJava)](http://javaops.ru/view/topjava)
-## [Почему мы?](http://javaops.ru/#why)
-
-## REST, REST-контроллеры, тестирование контроллеров Spring MVC
-# Для просмотра открыты видео [4](#--4-миграция-на-junit-5), [5](#-5-принципы-rest-rest-контроллеры), [6](#-6-тестирование-rest-контроллеров-jackson), [7](#-7-кастомизация-jackson-object-mapper), [8](#user-content--8-тестирование-rest-контроллеров-через-jsonassert-и-матчеры)
-- Не стоит стремиться прочитать все ссылки урока, их можно использовать как справочник. Гораздо важнее пройти основной материал урока и сделать домашнее задание
-- Обязательно посмотри правила работы с патчами на проекте
-- Делать Apply Patch лучше по одному непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (`Version Control -> Local Changes -> Ctrl+D`)
-- При первом Apply удобнее выбрать имя локального ченджлиста Name: Default. Далее все остальные патчи также будут в него попадать.
-- Код проекта обновляется и не всегда совпадает с видео (можно увидеть, как развивался проект). Изменения в проекте указываю после соответствующего патча.
-
-##  Разбор домашнего задания HW6
-
-###  1. HW6
-#### Apply 7_01_HW6_fix_tests.patch
-
-#### Apply 7_02_HW6_meals.patch
-
-> сделал фильтрацию еды через `get`: операция идемпотентная, можно делать в браузере обновление по F5
-
-### Внимание: чиним пути в следующем патче
-
-#### Apply 7_03_HW6_fix_relative_url_utf8.patch
-
--
- Relative paths in JSP
--
- Spring redirect: prefix
-
-###  2. HW6 Optional
-
-#### Apply 7_04_HW6_optional_add_role.patch
-
-#### `JdbcUserServiceTest` отвалились. Будем чинить в `7_06_HW6_jdbc_transaction_roles.patch`
-
-#### Apply 7_05_fix_hint_graph.patch
-
-- В `JpaUserRepositoryImpl.getByEmail` DISTINCT попадает в запрос, хотя он там не нужен. Это просто указание Hibernate
- не дублировать данные. Для оптимизации можно указать Hibernate делать запрос без
- distinct: [15.16.2. Using DISTINCT with entity queries](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct)
-- Бага [HINT_PASS_DISTINCT_THROUGH does not work if 'hibernate.use_sql_comments=true'](https://hibernate.atlassian.net/browse/HHH-13280). При `hibernate.use_sql_comments=false` все работает - в SELECT нет DISTINCT.
-- Тест `DataJpaUserServiceTest.getWithMeals()` не работает для admin (у админа 2 роли, и еда при JOIN дублируется). ...
-
-#### Apply 7_06_HW6_jdbc_transaction_roles.patch
-
-Еще интересные JDBC реализации: ...
-
-### Валидация для `JdbcUserRepository` через Bean Validation API
-
-#### Apply 7_07_HW6_optional_jdbc_validation.patch
-
-- [Валидация данных при помощи Bean Validation API](https://alexkosarev.name/2018/07/30/bean-validation-api/).
-
-На данный момент у нас реализована валидация сущностей только для jpa- и dataJpa-репозиториев. При работе
-через JDBC-репозиторий может произойти попытка записи в БД некорректных данных, что приведет к `SQLException` из-за нарушения
-ограничений, наложенных на столбцы базы данных. Для того чтобы перехватить невалидные данные еще до
-обращения в базу, воспользуемся API *javax.validation* (ее реализация `hibernate-validator` используется для проверки данных в Hibernate и будет использоваться в Spring Validation, которую подключим позже).
-В `ValidationUtil` создадим один потокобезопасный валидатор, который можно переиспользовать (см. *javadoc*).
-С его помощью в методах сохранения и обновления сущности в jdbc-репозиториях мы можем производить валидацию этой сущности: `ValidationUtil.validate(object);`
-Чтобы проверка не падала, `@NotNull Meal.user` пришлось пока закомментировать. Починим в 10-м занятии через `@JsonView`.
-
-### Отключение кэша в тестах:
-
-Вместо наших приседаний с `JpaUtil` и проверкой профилей мы можем ...
-
-#### Apply 7_08_HW06_optional2_disable_tests_cache.patch
-
-- [Example of PropertyOverrideConfigurer](https://www.concretepage.com/spring/example_propertyoverrideconfigurer_spring)
-- [Spring util schema](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#xsd-schemas-util)
-
-## Занятие 7:
-
-###  3. Тестирование Spring MVC
-
-
- Краткое содержание
-
-#### Тестирование Spring MVC
-
-Для более удобного сравнения объектов в тестах мы будем использовать библиотеку *Harmcrest* с Matcher'ами, которая
-позволяет делать сложные проверки. С *JUnit* по умолчанию подтягивается *Harmcrest core*, но нам потребуется расширенная версия:
-в `pom.xml` из зависимости JUnit исключим дочернюю `hamcrest-core` и добавим `hamcrest-all`.
-
-Для тестирования web создадим вспомогательный класс `AbstractControllerTest`, от которого будут наследоваться все
-тесты контроллеров. Его особенностью будет наличие `MockMvc` - эмуляции Spring MVC для тестирования web-компонентов.
-Инициализируем ее в методе, отмеченном `@PostConstruct`:
-
- ```
-mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilter(CHARACTER_ENCODING_FILTER).build();
- ```
-
-Для того чтобы в тестах контроллеров не популировать базу перед каждым тестом, пометим этот базовый тестовый класс аннотацией `@Transactional`.
-Теперь каждый тестовый метод будет выполняться в транзакции, которая будет откатываться после окончания метода и возвращать базу данных в исходное
-состояние. Однако теперь в работе тестов могут возникнуть нюансы, связанные с пропагацией транзакций: все
-транзакции репозиториев станут вложенными во внешнюю транзакцию теста. При этом, например, кэш первого уровня станет работать не
-так, как ожидается. Т. е. при таком подходе нужно быть готовыми к ошибкам: мы их увидим и поборем в тестах на обработку ошибок на последних занятиях TopJava.
-
-#### UserControllerTest
-
-Создадим тестовый класс для контроллера юзеров, он должен наследоваться от `AbstractControllerTest`.
-В `MockMvc` используется [паттерн проектирования Builder](https://refactoring.guru/ru/design-patterns/builder).
-
- ```
- mockMvc.perform(get("/users")) // выполнить HTTP метод GET к "/users"
- .andDo(print()) // распечатать содержимое ответа
- .andExpect(status().isOk()) // от контроллера ожидается ответ со статусом HTTP 200(ok)
- .andExpect(view().name("users")) // контроллер должен вернуть view с именем "users"
- .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) // ожидается, что клиент должен быть перенаправлен на "/WEB-INF/jsp/users.jsp"
- .andExpect(model().attribute("users", hasSize(2))) // в модели должен быть атрибут "users" размером = 2 ...
- .andExpect(model().attribute("users", hasItem( // ... внутри которого есть элемент ...
- allOf(
- hasProperty("id", is(START_SEQ)), // ... с аттрибутом id = START_SEQ
- hasProperty("name", is(USER.getName())) //... и name = user
- )
- )));
-}
- ```
-
-В параметры метода `andExpect()` передается реализация `ResultMatcher`, в которой мы определяем, как должен быть обработан ответ контроллера.
-
-
-
-#### Apply 7_09_controller_test.patch
-
-> - в `MockMvc` добавился `CharacterEncodingFilter`
-> - добавил [`AllActiveProfileResolver`](//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver) для возвращения массива профилей
-> - сделал вспомогательный метод `AbstractControllerTest.perform()`
-
-- Hamcrest
-- Unit Testing of Spring MVC Controllers
-
-###  4. [Миграция на JUnit 5](https://drive.google.com/open?id=16wi0AJLelso-dPuDj6xaGL7yJPmiO71e)
-
-
- Краткое содержание
-
-Для миграции на 5-ю версию JUnit в файле `pom.xml` поменяем зависимость `junit` на `junit-jupiter-engine` ([No need `junit-platform-surefire-provider` dependency in `maven-surefire-plugin`](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven)).
-Актуальную версию всегда можно посмотреть [в центральном maven-репозитории](https://search.maven.org/search?q=junit-jupiter-engine), берем только релизы (..-Mx означают предварительные milestone версии)
-Изменять конфигурацию плагина `maven-sureface-plugin` в новых версиях JUnit уже не требуется.
-JUnit5 не содержит в себе зависимости от *Harmcrest* (которую нам приходилось вручную
-отключать для JUnit4 в предыдущих шагах), поэтому исключение `hamcrest-core` просто удаляем.
-В итоге у нас останутся зависимости JUnit5 и расширенный Harmcrest.
-Теперь мы можем применить все нововведения пятой версии в наших тестах:
- 1. Для всех тестов теперь мы можем удалить `public`.
- 2. Аннотацию `@Before` исправим на `@BeforeEach` - теперь метод, который будет выполняться перед
-каждым тестом, помечается именно так.
- 3. В JUnit5 работа с исключениями похожа на JUnit4 версии 4.13: вместо ожидаемых исключений в параметрах аннотации `@Test(expected = Exception.class)` используется метод `assertThrows()`,
-в который первым аргументом мы передаем ожидаемое исключение, а вторым аргументом — реализацию функционального интерфейса `Executable` (код,
-в котором ожидается возникновение исключения).
- 4. Метод `assertThrows()` возвращает исключение, которое было выброшено в переданном ему коде. Теперь мы можем получить это исключение, извлечь из него сообщение с помощью
- `e.getMessage()` и сравнить с ожидаемым.
- 5. Для теста на валидацию при проверке предусловия, только при выполнении которого
-будет выполняться следующий участок кода (например, в нашем случае тесты на валидацию выполнялись
-только в jpa профиле), теперь нужно пользоваться утильным методом `Assumptions` (нам уже не требуется).
- 6. Проверку Root Cause - причины, из-за которой было выброшено пойманное исключение, мы будем делать позднее, при тестах на ошибки.
- 7. Из JUnit5 исключена функциональность `@Rule`, вместо них теперь нужно использовать `Extensions`, которые
-могут встраиваться в любую фазу тестов. Чтобы добавить их в тесты, пометим базовый тестовый класс аннотацией `@ExtendWith`.
-
-JUnit предоставляет нам набор коллбэков — интерфейсов, которые будут исполняться в определенный момент тестирования.
-Создадим класс `TimingExtension`, который будет засекать время выполнения тестовых методов.
-Этот класс будет имплементировать маркерные интерфейсы — коллбэки JUnit:
- - `BeforeTestExecutionCallback` - коллбэк, который будет вызывать методы этого интерфейса перед каждым тестовым методом.
- - `AfterTestExecutionCallback` - методы этого интерфейса будут вызываться после каждого тестового метода;
- - `BeforeAllCallback` - методы перед выполнением тестового класса;
- - `AfterAllCallback` - методы после выполнения тестового класса;
-
-Осталось реализовать соответствующие методы, которые описываются в каждом из этих интерфейсов, они и будут вызываться JUnit в нужный момент:
- - в методе `beforeAll` (который будет вызван перед запуском тестового класса) создадим спринговый утильный секундомер `StopWatch` для текущего тестового класса;
- - в методе `beforeTestExecution` (будет вызван перед тестовым методом) - запустим секундомер;
- - в методе `afterTestExecution` (будет вызван после тестового метода) - остановим секундомер.
- - в методе `afterAll` (который будет вызван по окончанию работы тестового класса) - выведем результат работы этого секундомера в лог;
-
-8. Аннотации `@ContextConfiguration` и `@ExtendWith(SpringExtension.class)` (замена `@RunWith`) мы можем заменить одной `@SpringJUnitConfiguration` (старые версии IDEA ее не понимают)
-
-
-
-#### Apply 7_10_JUnit5.patch
-
-> - [No need `junit-platform-surefire-provider` dependency in `maven-surefire-plugin`](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven)
-> - [Наконец пофиксили баг с `@SpringJUnitConfig`](https://youtrack.jetbrains.com/issue/IDEA-166549)
-
-- [JUnit 5 homepage](https://junit.org/junit5)
-- [Overview](https://junit.org/junit5/docs/snapshot/user-guide/#overview)
-- [10 интересных нововведений](https://habr.com/post/337700)
-- Дополнительно:
- - [Extension Model](https://junit.org/junit5/docs/current/user-guide/#extensions)
- - [A Guide to JUnit 5](http://www.baeldung.com/junit-5)
- - [Migrating from JUnit 4](http://www.baeldung.com/junit-5-migration)
- - [Before and After Test Execution Callbacks](https://junit.org/junit5/docs/snapshot/user-guide/#extensions-lifecycle-callbacks-before-after-execution)
- - [Conditional Test Execution](https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-conditional-execution)
- - [Third party Extensions](https://github.com/junit-team/junit5/wiki/Third-party-Extensions)
- - [Реализация assertThat](https://stackoverflow.com/questions/43280250)
-
-###  5. [Принципы REST. REST контроллеры](https://drive.google.com/open?id=1e4ySjV15ZbswqzL29UkRSdGb4lcxXFm1)
-
-
- Краткое содержание
-
-#### Принципы REST, REST-контроллеры
-
-> [REST](http://spring-projects.ru/understanding/rest/) - архитектурный стиль проектирования распределенных систем (типа клиент-сервер).
-
-Чаще всего в REST-сервер и клиент общаются посредством обмена JSON-объектами через HTTP-методы GET/POST/PUT/DELETE/PATCH.
-Особенностью REST является отсутствие состояния (контекста) взаимодействий клиента и сервера.
-
-В нашем приложении есть контроллеры для Admin и для User. Чтобы сделать их REST-контроллерами,
-заменим аннотацию `@Controller` на `@RestController`
-
-> Не поленитесь зайти через Ctrl+click в `@RestController`: к аннотации `@Controller` добавлена `@ResponseBody`. Т. е. ответ от нашего приложения будет не View, а данные в теле ответа.
-
-В `@RequestMapping`, кроме пути для методов контроллера (`value`), добавляем параметр `produces = MediaType.APPLICATION_JSON_VALUE`.
-Это означает, что в заголовки ответа будет добавлен тип `ContentType="application/json"` - в ответе от контроллера будет приходить JSON-объект.
-
-> Чтобы было удобно использовать путь к этому контроллеру в приложении и в тестах,
-> выделим путь к нему в константу REST_URL, к которой можно будет обращаться из других классов
-
-1. Метод `AdminRestController.getAll` пометим аннотацией `@GetMapping` - маршрутизация к методу по HTTP GET.
-
-2. Метод `AdminRestController.get` пометим аннотацией `@GetMapping("/{id}")`.
-В скобках аннотации указано, что к основному URL контроллера будет добавляться `id` пользователя - переменная, которая передается в запросе непосредственно в URL.
- Соответствующий параметр метода нужно пометить аннотацией `@PathVariable` (если имя в URL и имя аргумента метода не совпадают, в параметрах аннотации дополнительно нужно будет уточнить
- имя в URL. Если они совпадают, [этого не требуется](https://habr.com/ru/post/440214/).
-
-3. Метод создания пользователя `create` отметим аннотацией `@PostMapping` - маршрутизация к методу по HTTP POST.
- В метод мы передаем объект `User` в теле запроса (аннотация `@RequestBody`) в формате JSON (`consumes = MediaType.APPLICATION_JSON_VALUE`).
- При создании нового ресурса правило хорошего тона - вернуть в заголовке ответа URL созданного ресурса.
- Для этого возвращаем не `User`, а `ResponseEntity`, который мы можем с помощью билдера `ServletUriComponentsBuilder` дополнить заголовком ответа `Location` и вернуть статус `CREATED(201)`
- (если пойти в код `ResponseEntity.created` можно докопаться до сути, очень рекомендую смотреть в исходники кода).
-
-4. Метод `delete` помечаем `@DeleteMapping("/{id}")` - HTTP DELETE.
- Он ничего не возвращает, поэтому помечаем его аннотацией `@ResponseStatus(HttpStatus.NO_CONTENT)`. Статус ответа будет HTTP.204;
-
-5. Над методом обновления ставим `@PutMapping` (HTTP PUT). В аргументах метод принимает `@RequestBody User user` и `@PathVariable int id`.
-
-6. Метод поиска по `email` также помечаем `@GetMapping` и, чтобы не было конфликта маршрутизации с методом `get()`,
- указываем в URL добавку `/by`. В этот метод `email` передается как параметр запроса (аннотация `@RequestParam`).
-
-> **Все это СТАНДАРТ архитектурного стиля REST. НЕ придумывайте ничего своего в своих выпускных проектах! Это очень большая ошибка - не придерживаться стандартов API.**
-
-7. `ProfileRestController` выполняем аналогичным способом с учетом того, что пользователь имеет доступ только к своим данным.
-
-Если на данном этапе попытаться запустить приложение и обратиться к какому-либо методу контроллера, сервер ответит нам ошибкой со статусом 406,
-так как Spring не знает, как преобразовать объект User в JSON...
-
-
-
-#### Apply 7_11_rest_controller.patch
-
-- Понимание REST
-- JSON (JavaScript Object Notation)
-- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/)
-- [10 Best Practices for Better RESTful](https://medium.com/@mwaysolutions/10-best-practices-for-better-restful-api-cbe81b06f291)
-- [Best practices for rest nested resources](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources)
--
- Request mapping
-- [Лучшие практики разработки REST API: правила 1-7,15-17](https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/)
-- Дополнительно:
- - [Подборка практик REST](https://gist.github.com/Londeren/838c8a223b92aa4017d3734d663a0ba3)
- - JAX-RS vs Spring MVC
- - RESTful API для сервера – делаем правильно (Часть 1)
- - RESTful API для сервера – делаем правильно (Часть 2)
- - И. Головач.
- RestAPI
- - [value/name в аннотациях @PathVariable и @RequestParam](https://habr.com/ru/post/440214/)
-
-###  6. [Тестирование REST контроллеров. Jackson.](https://drive.google.com/open?id=1aZm2qoMh4yL_-i3HhRoyZFjRAQx-15lO)
-
-
- Краткое содержание
-
-Для работы с JSON добавляем в `pom.xml` зависимость `jackson-databind`.
-Актуальную версию библиотеки можно посмотреть в [центральном maven-репозитории](https://search.maven.org/artifact/com.fasterxml.jackson.core/jackson-databind).
-Теперь Spring будет автоматически использовать эту библиотеку для сериализации/десериализации объектов в JSON (найдя ее в *classpath*).
-Если сейчас запустить приложение и обратиться к методам REST-контроллера, то оно выбросит `LazyInitializationException`.
-Оно возникает из-за того, что у наших сущностей есть лениво загружаемые поля, отмеченные `FetchType.LAZY` - при загрузке сущности из базы вместо такого поля подставится Proxy, который и должен вернуть
-реальный экземпляр этого поля при первом же обращении. Jackson при сериализации в JSON использует все поля сущности,
-и при обращении к *Lazy*-полям возникает исключение, так как сессия работы с БД в этот момент уже закрыта, и нужный объект
-не может быть инициализирован. Чтобы Jackson игнорировал эти поля, пометим их аннотацией `@JsonIgnore`.
-
-Теперь при запуске приложения REST-контроллер будет работать. Но при получении JSON-объектов мы можем увидеть, что Jackson сериализовал объект
-через геттеры (например, в ответе есть поле `new` от метода `Persistable.isNew()`).
-Чтобы учитывались только поля объектов, добавим над `AbstractBaseEntity`:
-````java
-@JsonAutoDetect(fieldVisibility = ANY, // jackson видит все поля
- getterVisibility = NONE, // ... но не видит геттеров
- isGetterVisibility = NONE, //... не видит геттеров boolean-полей
- setterVisibility = NONE) // ... не видит сеттеров
-````
-Теперь все сущности, унаследованные от базового класса, будут сериализоваться/десериализоваться через поля.
-
-
-
-#### Apply 7_12_rest_test_jackson.patch
-
-- [Jackson databind github](https://github.com/FasterXML/jackson-databind)
-- [Jackson Annotation Examples](https://www.baeldung.com/jackson-annotations)
-
-###  7. [Кастомизация Jackson Object Mapper](https://drive.google.com/open?id=1CM6y1JhKG_yeLQE_iCDONnI7Agi4pBks)
-
-
- Краткое содержание
-
-Сейчас, чтобы не сериализовать *Lazy*-поля, мы должны пройтись по каждой сущности и
-вручную пометить их аннотацией `@JsonIgnore`. Это неудобно, засоряет код и допускает возможные ошибки. К тому же,
-при некоторых условиях, нам иногда нужно загрузить и в ответе передать эти *Lazy*-поля.
-Чтобы запретить сериализацию Lazy-полей для всего проекта, подключим в `pom.xml` библиотеку `jackson-datatype-hibernate`.
-Также изменим сериализацию/десериализацию полей объектов в JSON: не через аннотацию `@JsonAutoDetect`, а в классе `JacksonObjectMapper`, который
-унаследуем от `ObjectMapper` (стандартный Mapper, который использует Jackson) и сделаем в нем другие настройки.
-В конструкторе:
-- регистрируем `Hibernate5Module` - модуль `jackson-datatype-hibernate`, который не делает сериализацию ленивых полей.
-- модуль для корректной сериализации `LocalDateTime` в поля JSON - `JavaTimeModule` модуль библиотеки `jackson-datatype-jsr310`
-- запрещаем доступ ко всем полям и методам класса и потом разрешаем доступ только к полям
-- не сериализуем null-поля (`setSerializationInclusion(JsonInclude.Include.NON_NULL)`)
-
-Чтобы подключить наш кастомный `JacksonObjectMapper` в проект, в конфигурации `spring-mvc.xml` к
-настройке `` добавим `MappingJackson2HttpMessageConverter`, который будет использовать наш маппер.
-
-
-
-
-#### Apply 7_13_jackson_object_mapper.patch
-
-- Сериализация hibernate lazy-loading с помощью
- jackson-datatype-hibernate
-- Handle Java 8 dates with Jackson
-- Дополнительно:
- - Jackson JSON
- Serializer & Deserializer
-
-###  8. [Тестирование REST-контроллеров через JSONassert и Матчеры](https://drive.google.com/open?id=1oa3e0_tG57E71g6PW7_tfb3B61Qldctl)
-
-
- Краткое содержание
-
-Сейчас в тестах REST-контроллера мы проводим проверку только на статус ответа и тип возвращаемого контента. Добавим проверку содержимого ответа.
-
-#### 7_14_json_assert_tests
-
-Чтобы сравнивать содержимое ответа контроллера в виде JSON и сущность, воспользуемся библиотекой
-`jsonassert`, которую подключим в `pom.xml` со scope *test*.
-
-Эта библиотека при сравнении в тестах в качестве ожидаемого значения ожидает от
-нас объект в виде JSON-строки. Чтобы вручную не преобразовывать объекты в JSON и не
-хардкодить их в виде строк в наши тесты, воспользуемся Jackson.
-Для преобразования объектов в JSON и обратно создадим утильный класс `JsonUtil`, в котором
-с помощью нашего `JacksonObjectMapper` и будет конвертировать объекты.
-И мы сталкиваемся с проблемой: `JsonUtil` - утильный класс и не является
-бином Spring, а для его работы требуется наш кастомный маппер, который находится под управлением
-Spring и расположен в контейнере зависимостей. Поэтому, чтобы была возможность получить
-наш маппер из других классов, сделаем его синглтоном и сделаем в нем статический
-метод, который будет возвращать его экземпляр. Теперь `JsonUtil` сможет его получить.
-И нам нужно указать Spring, чтобы он не создавал второй экземпляр этого объекта, а клал в свой контекст существующий.
-Для этого в конфигурации `spring-mvc.xml` определим factory-метод, с помощью которого Spring должен
-получить экземпляр (instance) этого класса:
-```xml
-
-```
-а в конфигурации `message-converter` вместо создания бина просто сошлемся на сконфигурированный `objectMapper`.
-
-Метод `ContentResultMatchers.json()` из `spring-test` использует библиотеку `jsonassert` для сравнения 2-х JSON строк: одну из ответа контроллера и вторую -
-JSON-сериализация `admin` без поля `registered` (это поле инициализируется в момент создания и отличается).
-В методе `JsonUtil.writeIgnoreProps` мы преобразуем объект `admin` в мапу, удаляем из нее игнорируемые поля и снова сериализуем в JSON.
-
-Также сделаем тесты для утильного класса `JsonUtil`. В тестах мы записываем
-объект в JSON-строку, затем конвертируем эту строку обратно в объект и сравниваем с исходным. И то же самое делаем со списком объектов.
-
-#### 7_15_tests_refactoring
-
-**`RootControllerTest`**
-
-Сделаем рефакторинг `RootControllerTest`. Ранее мы в тесте получали модель, доставали из нее сущности и с помощью `hamcrest-all`
-производили по одному параметру их сравнение с ожидаемыми значениями.
-Метод `ResultActions.andExpect()` позволяет передавать реализацию интерфейса `Matcher`, в котором можно делать любые сравнения.
-Функциональность сравнения списка юзеров по ВСЕМ полям у нас уже есть - мы просто делегируем сравнение объектов в `UserTestData.MATCHER`.
-При этом нам больше не нужен `harmcrest-all`, нам достаточно только `harmcrest-core`.
-
-**`MatcherFactory`**
-
-Теперь вместо `jsonassert` и сравнения JSON-строк в тестах контроллеров сделаем сравнения JSON-объектов через `MatcherFactory`.
-Преобразуем ответ контроллера из JSON в объект и сравним с эталоном через уже имеющийся у нас матчер.
-Вместо сравнения JSON-строк в метод `andExpect()` мы будем передавать реализации интерфейса `ResultMatcher` из `MATCHER.contentJson(..)`.
-
-`MATCHER.contentJson(..)` принимают ожидаемый объект и возвращают для него `ResultMatcher` с реализацией единственного метода `match(MvcResult result)`,
-в котором делегируем сравнение уже существующим у нас матчерам.
-Мы берем JSON-тело ответа (`MatcherFactory.getContent`), десериализуем его в объект (`JsonUtil.readValue/readValues`) и сравниваем через имеющийся `MATCHER.assertMatch`
-десериализованный из тела контроллера объект и ожидаемое значение.
-
-> Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил.
-
-**`AdminRestControllerTest`**
-
-- `getByEmail()` - сделан по аналогии с тестом `get()`. Дополнительно нужно добавить в строку URL параметры запроса.
-- `delete()` - выполняем HTTP.DELETE. Проверяем статус ответа 204. Проверяем, что пользователь удален.
-
-> Раньше я получал всех users из базы и проверял, что среди них нет удаленного. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении/удалении новых тестовых данных.
-
-- `update()` - выполняем HTTP.PUT. В тело запроса подаем сериализованный `JsonUtil.writeValue(updated)`. После выполнения проверяем, что объект в базе обновился.
-- `create()` - выполняем HTTP.POST аналогично `update()`. Но сравнить результат мы сразу не можем, т. к. при создании объекта ему присваивается `id`.
- Поэтому мы извлекаем созданного пользователя из ответа (`MATCHER.readFromJson(action)`), получаем его `id`, и уже с этим `id` эталонный объект мы можем сравнить с объектом в ответе контроллера и со
- значением в базе.
-- `getAll()` - аналогично `get()`. Список пользователей из ответа в формате JSON сравниваем с эталонным списком (`MATCHER.contentJson(admin, user)`).
-
-Тесты для `ProfileRestController` выполнены аналогично.
-
-
-
-#### Apply 7_14_json_assert_tests.patch
-
-> - В `JsonUtil.writeIgnoreProps` вместо цикла по мапе сделал `map.keySet().removeAll`
-
-- [JSONassert](https://github.com/skyscreamer/JSONassert)
-- [Java Code Examples for ObjectMapper](https://www.programcreek.com/java-api-examples/index.php?api=com.fasterxml.jackson.databind.ObjectMapper)
-
-#### Apply 7_15_tests_refactoring.patch
-
-> - Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил.
-> - Раньше в тестах я для проверок получал всех users из базы и сравнивал с эталонным списком. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении/удалении новых тестовых данных.
-
-- [Java @SafeVarargs Annotation](https://www.baeldung.com/java-safevarargs)
-
-###  9. Тестирование через SoapUI. UTF-8
-
-
- Краткое содержание
-
-SoapUI - это один из инструментов для тестирования API приложений, которые работают по REST и по SOAP.
-Он позволяет нам по протоколу HTTP дернуть методы нашего API и увидеть ответ контроллеров.
-
-Если в контроллер мы добавим метод, который в теле ответа будет возвращать текст на кириллице, то увидим, что кодировка теряется.
-Для сохранения кодировки используем `StringHttpMessageConverter`, который конфигурируем в `spring-mvc.xml`.
-При этом мы должны явно указать, что конвертор будет работать только с текстом в кодировке *UTF-8*.
-
-
-
-#### Apply 7_16_soapui_utf8_converter.patch
-
-- Инструменты тестирования REST:
- - SoapUI
- - Написание HTTP-запросов с помощью
- Curl.
- Для Windows 7 можно использовать Git Bash, с Windows 10 v1803 можно прямо из консоли. Возможны проблемы с UTF-8:
- - [CURL doesn't encode UTF-8](https://stackoverflow.com/a/41384903/548473)
- - [Нстройка кодировки в Windows](https://support.socialkit.ru/ru/knowledge-bases/4/articles/11110-preduprezhdenie-obnaruzhenyi-problemyi-svyazannyie-s-raspoznavaniem-russkih-simvolov)
- - **[IDEA: Tools->HTTP Client->...](https://www.jetbrains.com/help/idea/rest-client-tool-window.html)**
- - Postman
- - [Insomnia REST client](https://insomnia.rest/)
-
-**Импортировать проект в SoapUI из `config\Topjava-soapui-project.xml`. Response смотреть в формате JSON.**
-
-> Проверка UTF-8: http://localhost:8080/topjava/rest/profile/text
-
-[ResponseBody and UTF-8](http://web.archive.org/web/20190102203042/http://forum.spring.io/forum/spring-projects/web/74209-responsebody-and-utf-8)
-
-##  Ваши вопросы
-
-> Зачем у нас и UI-контроллеры, и REST-контроллеры? То есть в общем случае backend-разработчику недостаточно предоставить REST API и RestController?
-
-В общем случае нужны и те, и другие. REST обычно используют для отдельного UI (например, на React или Angular) или для
-интеграции / мобильного приложения. У нас REST-контроллеры используются только для тестирования. UI-контроллеры используем для
-нашего приложения на JSP шаблонах. Таких сайтов без богатой UI логики тоже немало. Например https://javaops.ru/ :)
-Разница в запросах:
-
-- для UI используются только GET и POST
-- при создании/обновлении в UI мы принимаем данные из формы `application/x-www-form-urlencoded` (посмотрите
- вкладку `Network`, не в формате JSON)
-- для REST API запросы GET, POST, PUT, DELETE, PATCH и возвращают только данные (обычно JSON)
-
-...и в способе авторизации:
-
-- для REST API у нас будет базовая авторизация
-- для UI - через cookies
-
-Также часто бывают смешанные сайты, где есть и отдельное JS приложение, и шаблоны.
-
-> При выполнении тестов через MockMvc никаких изменений на базе не видно, почему оно не сохраняет?
-
-`AbstractControllerTest` аннотируется `@Transactional` - это означает, что тесты идут в транзакции, и после каждого
-теста JUnit делает rollback базы.
-
-> Что получается в результате выполнения запроса `SELECT DISTINCT(u) FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email`? В чем разница в SQL без `DISTINCT`.
-
-Запросы SQL можно посмотреть в логах. Т. е. `DISTINCT` в `JPQL` влияет на то, как Hibernate обрабатывает дублирующиеся
-записи (с `DISTINCT` их исключает). Результат можно посмотреть в тестах или приложении, поставив брекпойнт. По
-поводу `SQL DISTINCT` не стесняйтесь пользоваться google,
-например, [оператор SQL DISTINCT](http://2sql.ru/novosti/sql-distinct/)
-
-> В чем заключается расширение функциональности hamcrest в нашем тесте, что нам пришлось его отдельно от JUnit прописывать?
-
-hamcrest-all используется в проверках `RootControllerTest`: `org.hamcrest.Matchers.*`
-
-> Jackson мы просто подключаем в помнике, и Spring будет с ним работать без любых других настроек?
-
-Да, Spring смотрит в classpath и если видит там Jackson, то подключает интеграцию с ним.
-
-> Где-то слышал, что любой ресурс по REST должен однозначно идентифицироваться через url без параметров. Правильно ли задавать URL для фильтрации в виде `http://localhost/topjava/rest/meals/filter/{startDate}/{startTime}/{endDate}/{endTime}` ?
-
-Так делают только при
-отношении
-агрегация, например, если давать админу право смотреть еду любого юзера, URL мог бы быть похож
-на `http://localhost/topjava/rest/users/{userId}/meals/{mealId}` (не рекомендуется, см. ссылку ниже). В случае критериев
-поиска или страничных данных они передаются как параметр. Смотри также:
-
-- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/)
-- 10 Best Practices
- for Better RESTful
-- [REST resource hierarchy (если кратко: не рекомендуется)](https://stackoverflow.com/questions/15259843/how-to-structure-rest-resource-hierarchy)
-
-> Что означает конструкция в `JsonUtil`: `reader.readValues(json)`;
-
-См. Generic Methods. Когда компилятор
-не может вывести тип, можно его уточнить при вызове generic метода. Неважно, static или нет.
-
-##  Домашнее задание HW07
-
-- 1: Добавить тесты контроллеров:
- - 1.1 `RootControllerTest.getMeals` для `meals.jsp`
- - 1.2 Сделать `ResourceControllerTest` для `style.css` (проверить `status` и `ContentType`)
-- 2: Реализовать `MealRestController` и протестировать его через `MealRestControllerTest`
- - 2.1 следите, чтобы url в тестах совпадал с параметрами в методе контроллера. Можно добавить
- логирование `` для проверки маршрутизации.
- - 2.2 в параметрах `getBetween` принимать `LocalDateTime` (конвертировать
- через @DateTimeFormat with Java
- 8 Date-Time API), пока без проверки на `null` (используя `toLocalDate()/toLocalTime()`, см. Optional п. 3). В
- тестах передавать в формате `ISO_LOCAL_DATE_TIME` (
- например `'2011-12-03T10:15:30'`).
-
-### Optional
-
-- 3: Переделать `MealRestController.getBetween` на параметры `LocalDate/LocalTime` c раздельной фильтрацией по
- времени/дате, работающий при `null` значениях (см. демо и `JspMealController.getBetween`)
- . Заменить `@DateTimeFormat` на свои LocalDate/LocalTime конверторы или форматтеры.
- - Spring Type
- Conversion
- - Spring Field
- Formatting
- -
- Difference between Spring MVC formatters and converters
-- 4: Протестировать `MealRestController` (SoapUI, Curl, IDEA Test RESTful Web Service, Postman). Запросы `curl` занести
- в отдельный `md` файл (или `README.md`)
-- 5: Добавить в `AdminRestController` и `ProfileRestController` методы получения пользователя вместе с
- едой (`getWithMeals`, `/with-meals`).
- - [Jackson – Bidirectional Relationships](https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion)
-
-### Optional 2
-
-- 6: Сделать тесты на методы контроллеров `getWithMeals()` (п. 5)
-
-**На следующем занятии используется JavaScript/jQuery. Если у вас там
-пробелы, пройдите его основы**
----------------------
-
-##  Типичные ошибки и подсказки по реализации
-
-- 1: Ошибка в тесте _Invalid read array from JSON_ обычно расшифровывается немного ниже: читайте внимательно.
-- 2: Jackson и неизменяемые объекты (для
- сериализации `MealTo`)
-- 3: Если у meal, приходящий в контроллер, поля `null`, проверьте `@RequestBody` перед параметром (данные приходят в
- формате JSON)
-- 4: При проблемах с собственным форматтером убедитесь, что в конфигурации `[JVM Ecosystem Report 2021](https://snyk.io/jvm-ecosystem-report-2021/)
->показывает, что Spring является абсолютным
->лидером среди фреймворков.
-
-На первом месте упоминается **Spring Boot**, а на втором - **Spring MVC**.
-
-Здесь хотелось бы внести некоторую ясность.
-"Сердцем" Spring является Spring Core, который реализует Dependency
-Injection.
-Этот компонент, как правило, используется во всех остальных
-проектах Spring Framework.
-
-Spring Boot - это надстройка над Spring Framework,
-которая позволяет быстро создавать приложения разных типов,
-использовать автоматическую конфигурацию для некоторых
-компонентов и использовать некоторые удобные встроенные
-инструменты, например, готовые инструменты для мониторинга
-работы приложения (Spring Actuator).
-
-Если мы создаем web-приложение, то мы можем создавать его с
-помощью Spring Boot, но Spring MVC, который является именно
-web-фреймворком, также будет присутствовать в таком приложении
-и играть ключевую роль.
-
-Spring Boot позволяет создавать очень простые приложения без
-единой строчки конфигурации. Но как только мы пытаемся
-создать что-то более сложное, нам все равно приходится
-создавать конфигурационные файлы и классы и настраивать все вручную
-точно так же, как в традиционном Spring-приложении.
-По своему опыту могу сказать, что на реальных проектах
-такая ситуация возникает в большинстве случаев.
-Например, как только нужно настроить подключение
-к нескольким базам данных, а не к одной, мы вынуждены
-создавать конфигурационные классы вручную.
-
-В этом случае Spring Boot в какой-то степени может даже
-навредить вашей продуктивности, потому что вам придется
-разбираться, какие автоконфигурационные классы отключить, и т. д.
-
-Мы изучаем Spring довольно глубоко.
-Традиционно хорошо показал себя подход, при котором
-вначале изучается чистый Spring Framework, после чего
-мы переходим к использованию Spring Boot. Такой подход
-сохраняется и в этой версии курса. Без этого Spring Boot
-будет для вас "черным ящиком", который вам будет очень
-сложно понять, настраивать и отлаживать.
-
-### ORM frameworks
-Отчеты, которые мы использовали, обычно не включают информацию
-о самых популярных ORM-фреймворках.
-
->Известно,
->что **Hibernate** является достаточно популярным фреймворком
->и наиболее часто упоминается в вакансиях, если сравнивать
->его с другими ORM-фреймворками и инструментами для работы Java-приложения
->с базами данных.
-
-Вот одна из статей, которая также говорит о
-популярности Hibernate: [Top 5 Java ORM tools - 2022](https://www.knowledgefactory.net/2021/09/top-java-orm-tools-20XX.html)
-
-**ORM** или **Object-relational-mapping** можно перевести как
-"объектно-реляционное отображение (преобразование)".
-Это техника, которая позволяет создать виртуальную базу
-данных с помощью объектно ориентированного языка
-программирования и взаимодействовать с ней, в то время
-как взаимодействие уже с реальной базой данных выполняется
-фреймворком незаметно для нас.
-Это облегчает работу, позволяет работать с привычными
-и удобными для использования Java-объектами, вызывая
-их методы, вместо того, чтобы писать SQL-запросы к базе данных и код для интерпретации полученных данных.
-
->Hibernate является реализацией спецификации JPA,
->так же, как и менее популярный EclipseLink.
-
-Говоря простым языком, JPA (Java Persistence API) - это
-набор интерфейсов без реализации для работы с ORM,
-который включен в стандарт Java EE. Отдельные провайдеры
-могут предоставлять свои реализации этого интерфейса.
-
-Недостатками JPA и его реализации Hibernate являются удар по производительности
-и недостаточная гибкость. Вопрос производительности частично
-может быть решен с помощью различных техник оптимизации кода,
-написанного с использованием JPA, однако для некоторых
-приложений тот оверхед, который несет в себе JPA, является
-недопустимым, и вы можете увидеть на некоторых проектах
-использование других подходов, от использования
-чистого JDBC до использования MyBatis, JOOQ или каких-то
-альтернативных решений.
-
-В реальных приложениях те запросы к базе данных,
-которые создают наибольшую нагрузку на систему,
-могут быть оптимизированы для улучшения производительности: для
-них могут быть применены JDBC, альтернативные
-JPA-фреймворки, также запросы могут быть написаны
-с помощью нативных SQL-запросов в JPA.
-Для остальных запросов, которые не приводят к
-большой нагрузке на систему, в том же приложении может
-быть применен Hibernate.
-Таким образом, в одном приложении могут быть применены одновременно разные технологии.
-
-
-На курсе вы изучите, как работать с БД с помощью нескольких
-технологий: JDBC (а точнее, Spring JDBC template),
-а также JPA и Spring DataJPA.
-Вы сами не только увидите, но и почувствуете
-при написании кода в ваших домашних заданиях плюсы и минусы каждого подхода.
-
-### Обзор пройденных тем
-Давайте подведем промежуточные итоги по результатам этого урока.
-
-В этом уроке мы:
-
-* познакомились с тем, что такое фреймворки
-и для чего они нужны;
-* посмотрели статистику, которая определяет
-Spring Framework как самый популярный фреймворк на рынке;
-* познакомились с ORM - подходом для упрощения работы
-с базами данных с использованием объектно-ориентированного подхода;
-* узнали про JPA (Java Persistence API) - протокол для работы с
-ORM - и про реализации JPA, среди которых Hibernate является самой популярной;
-* обсудили недостатки подхода ORM и познакомились с инструментами,
-которые могут быть использованы в качестве альтернативы.
-
-В следующем коротком уроке мы завершим обзор используемых технологий.
-Увидимся в следующем видео.
diff --git a/doc/video2.3.md b/doc/video2.3.md
deleted file mode 100644
index af9506cb4596..000000000000
--- a/doc/video2.3.md
+++ /dev/null
@@ -1,115 +0,0 @@
-## Обзор наиболее востребованных технологий. Тренды
-
-##  [Видео](https://drive.google.com/file/d/1XcTRkArj2guek9OiPuFEq_U1V4Dg0N-j)
-
-### Тренд на отказ от reflection
-
-Говоря о трендах, хотелось бы сказать, что в настоящее
-время развиваются фреймворки, построенные на отказе
-от использования Reflection API.
-Dependency injection в Spring, сериализация с помощью
-Jackson построены на использовании Reflection, что
-сильно бьет по производительности, но было достаточно
-удобным решение до недавнего времени.
-Фреймворк Micronaut полностью построен на отказе от
-Reflection, и это дает существенный прирост
-производительности при измерении ряда параметров.
-Micronaut использует продвинутый компилятор и
-генерацию байткода, что позволяет создать все
-бины (управляемые фреймворком объекты) в ходе компиляции.
-
-Пока доля Micronaut очень мала, но он используется
-в продакшене. Я предполагаю, что в какой-то момент
-Spring может также внедрить подобный подход.
-
-### Тренд на развитие реактивного программирования
-
-Продолжает развиваться реактивное программирование,
-которое является в какой-то степени новой парадигмой
-в программировании.
-
-Spring развивает свой реактивный фреймворк Spring WebFlux,
-который поддерживает библиотеку Reactor.
-
-
-Ряд современных задач не решается традиционными
-методами, такими как блокирующий Input/Output,
-HTTP-протокол. Одним из решений данных вопросов
-является применение подходов реактивного
-программирования (Reactor, RxJava), использование
-новых протоколов передачи данных (RSocket).
-У нас также готовится курс [ReactJava](https://javaops.ru/#inprogress) как
-продолжение TopJava на реактивном стеке.
-
-### Тренд на микросервисы, тезисы
-
-ПО становиться большим и сложным. Очень большое приложение
-сложно поддерживать одной командой, управлять его жизненным
-циклом - разработкой, релизами.
-
-Если в большое монолитное приложение добавляется новая
-функциональность приходиться выполнять повторного тестирование
-всей большой системы, в результате срок выхода новых фич увеличивается.
-
-Микросервисная архитектура позволяет решить эту проблему - она
-предполагает разделение большого приложения на отдельные
-приложения-модули, которые взаимодействуют друг с другом.
-Над одним модулем-сервисом может работать один человек или
-небольшая команда. У такого модуля-сервиса будет отдельный
-Git-репозиторий, он может быть задеплоен (развернут) независимо
-от остальной системы. При таком подходе, команды могут вести работу
-над различными сервисами параллельно, параллельно деплоить
-их на серверах. При внесении изменений в микросервис “А”,
-вероятность вызывать проблемы в микросервисе “B” снижается.
-У разных микросервисов могут быть собственные базы данных.
-Это также повышает надежность и гибкость. Например, если по каким то причинам после обновления банковского микросервиса, ответственного за выдачу кредитов возникли сбои и база данных оказалась недоступна, другие компоненты системы, имеющие отдельные базы данных, продолжат свою работу без сбоев.
-
-Это существенно упрощает доработку, тестирование и деплой таких систем.
-
-Использование микросервисов дает большую гибкость в выборе
-технологий. Нам не нужно ограничивать себя в использовании
-технологий для того, чтобы все стандартизировать внутри
-одной большой системы. Работая с отдельными микросервисами,
-мы можем выбрать идеально подходящие технологии для отдельных задач.
-Например, один микросервис может использовать реляционные
-базы данных, а второй эффективнее решает свою задачу
-используя NoSQL базу данных или in memory базу данных.
-
-Еще одно важное преимущество, которое дает микросервисная
-архитектура это возможность легко масштабировать систему горизонтально.
-
-Предположим, что банк испытывает быстрый рост обращений
-об открытии новых счетов.
-
-Если банк использует монолитную архитектуру,
-то есть одно большое приложение, ему может потребоваться
-запускать отдельный экземпляр всего своего приложения
-и направлять часть обращений от клиентов на этот экземпляр (instance).
-
-Если же банк использует микросервисную архитектуру,
-он может запустить несколько экземпляров только сервиса,
-ответственного за открытие новых счетов и распределять
-нагрузку между этими экземплярами.
-При этом микросервис, ответственный за выдачу кредитов,
-который не испытывает повышенной нагрузки, может остаться
-в одном экземпляре. При этом увеличение числа микросервисов,
-ответственных за открытие счетов может происходить автоматически
-с помощью инструментов DevOps и Kubernetes.
-
-Однако использование микросервисов также создает сложности.
-Разработка систем, построенных на микросервисной архитектуре
-на порядок сложнее. Обслуживание таких систем также
-существенно сложнее.
-
-Компания должна обслуживать множество серверов,
-обеспечивать мониторинг каждого микросервиса и так далее.
-Также security таких систем существенно сложнее,
-поскольку мы имеем дело с множеством приложений,
-коммуницирующих друг с другом и все эти коммуникации
-должны быть безопасными.
-
-
-Следующий, готовый стать самым популярным
-после TopJava курс - [Микросервисы](https://javaops.ru/view/cloudjava).
-
-[Первое занятие открытое](https://javaops.ru/view/cloudjava/lesson01)
diff --git a/doc/video2.4.md b/doc/video2.4.md
deleted file mode 100644
index 14041e795564..000000000000
--- a/doc/video2.4.md
+++ /dev/null
@@ -1,24 +0,0 @@
-## 2.4 Обзор наиболее востребованных технологий. Обзор разрабатываемого приложения
-
-##  [Видео](https://drive.google.com/file/d/1LHI18LZK1MRIEBpVe3WjCE890EydN5Gz)
-
-### О работе с фронтендом и JavaScript
-Курс включает себя минимальную практику работы с JavaScript.
-Почему это нужно?
-В вакансиях backend разработчиков, как правило, отсутствуют
-требования глубокого знания JavaScript, но каждый разработчик
-должен уметь пользовать DevTools - инструментами разработчика
-в браузере, чтобы при работе над рабочими задачами по меньшей
-мере иметь возможность понять, происходит ли ошибка на стороне
-фронтенда или на стороне бэкенда, увидеть, какие данные уходят
-на сервер с фронтенда и так далее.
-
-Также, конечно, минимальные знания JavaScript приятны, поскольку
-они дают вам возможность написать свой несложный проект хотя
-бы с минимальным фронтендом.
-
-Существуют также fullstack-разработчики, от которых требуется
-способность полноценно решать задачи фронтенда на продакшене.
-Это требует довольно глубокого знания фронтенд-технологий и
-таких фреймворков, как Angular или React. Это требует много
-времени, и такая подготовка не входит в курс TopJava.
\ No newline at end of file
diff --git a/doc/video3.md b/doc/video3.md
deleted file mode 100644
index 1821aac3f3af..000000000000
--- a/doc/video3.md
+++ /dev/null
@@ -1,168 +0,0 @@
-## Рекомендуемые подходы обучения на курсе
-
-##  [Видео](https://drive.google.com/file/d/1v5sVL8ivNvSXEPVlrYibFLD5byywRkmT)
-
-В предыдущих уроках мы сделали обзор технологий,
-которые будут использоваться в нашем курсе и создаваемом проекте.
-Теперь давайте поговорим о том, как начать
-изучать эти технологии и практиковаться в их использовании.
-
----
-В 1980 году National Training Laboratories в
-США провели исследования эффективности разных
-способов обучения.
-Выяснилось, что у лекций и чтения книг крайне
-низкая эффективность — всего 5-10%.
-Дальше идет просмотр видео лекций и прослушивание аудио.
-
-Максимальная эффективность в 90% — это обучение
-людьми других людей — менторинг и немедленное
-применение полученных знаний на практике.
-
-
-
-
-
-Я хочу подчеркнуть этот момент.
-
->Когда вы смотрите видео, вам может казаться, что вы все понимаете, но, поверьте, когда вы попытаетесь повторить это самостоятельно, у вас возникнет множество вопросов и сложностей. Практика - это важнейшая часть обучения, не пропускайте ее.
-
-На нашем курсе мы:
-- обсуждаем занятия с коллегами и преподавателями в Slack - эффективность 50%
-- выполняем практические домашние задания по каждой пройденной теме - 75%
-- помогаем коллегам и разрабатываем собственный выпускной проект - 90%
-
-### О проверке домашних заданий
-Также очень важная часть обучения - проверка
-ваших домашних заданий и ревью выпускного
-проекта нашими кураторами.
-
->**Это самый эффективный способ научиться программировать!**
-
-При устройстве на работу, на собеседовании обязательно
-задавай вопрос про ревью кода.
-Если его нет, фирма занимается разработкой
-непрофессионально? и рост там будет достаточно
-медленным и ограниченным.
-
-В ревью укажут именно твои ошибки
-в стиле, структурах данных, алгоритмах и кодировании.
-До вечера вторника участники шлют ссылку
-на свой GitHub-репозиторий с домашним
-заданием занятия, проверка делается
-ассистентами, результат пишется в Slack.
-Получается эффективно и оперативно.
-
-После проверки можно исправить замечания
-и пройти ее еще раз.
-
-В конце стажировки делается ревью вашего выпускного проекта.
-
-
-### Участие на стажировке: ожидания и реальность
-
-Давайте кратко обсудим, чем курс TopJava является
-и чем он не является, что следует от него ожидать
-и чего не следует.
-
-#### 1-й тип ложного представления о стажировке:
-
->Я увижу, как с нуля строится web-приложение A с использованием технологий B, просмотрю видео по темам, этого будет достаточно
-
-
-
-Почему такой подход не верный:
-TopJava - это стажировка, поэтому НЕ рассчитывайте
-пройти ее на диване с пакетом поп-корна.
-Тебе придется на ней РАБОТАТЬ (выполнять ДЗ,
-самостоятельно решать какие-то задачи, читать логи,
-дебажить, ходить на StackOverflow и даже думать
-об этом, засыпая)
-
-#### 2-й тип ложного представления о стажировке:
->Меня научат шаблонам работы с технологией А, и я
-> смогу их применять в любой ситуации
-
-Почему это тоже не вполне верный подход:
-Используемые на стажировке технологии представляют
-собой инструменты, которые позволяет сделать
-что-то проще. Мы поделимся практикой их использования,
-неочевидными особенностями и т. п., покажем
-"грабли", на которые вы рано или поздно наступите.
-Нет гарантии, что, устроившись на работу, вы
-увидите точно такие же подходы.
-Все проекты и команды индивидуальны: используются
-различные инструменты и различные решения.
-Столкновение с технологиями, с которыми
-ты ранее не был знаком, - это нормальная
-часть жизни любого программиста. Нужно быть к этому готовым.
-
-Хорошее представление о решении проблем
-дает поиск на StackOverflow, где почти
-на любую проблему дается большое количество
-вариантов решения. Поиск решений и выбор
-лучшего - это основная работа Java-разработчика,
-и мы максимально постараемся этому научить:
-каждый раз в конкретной ситуации вы должны
-будете САМИ думать, что применять и как.
-Выполняя домашние задания, вы должны приложить
-все усилия, чтобы самостоятельно найти решение.
-Далее в начале следующего занятия вы также
-посмотрите разбор решения, подготовленного
-Григорием Кислиным.
-
-#### 3-й вариант ошибочного представления о стажировке TopJava связан с неверным представлением о том зачем нужна проверка домашних заданий. Например, человек может ошибочно рассуждать так:
-
->я хочу проходить стажировку с проверкой ДЗ,
-> чтобы мне рассказали, как нужно правильно выполнять задания
-
-Задача проверки не в том, чтобы общими усилиями
-написать код, который ты и так увидишь в разборе.
-Если у тебя что-то не получается, наша
-задача - не найти ошибку/подебажить за тебя/почитать
-логи и т. п., а подсказать способ самостоятельно
-найти решение (хотя для этого мы сначала сами
-ищем/дебажим/читаем)
-Вторая важная задача, которую решает
-проверяющий, - увидеть то, что не увидел ты.
-Когда ты сдаешь задание на ревью, тебе
-может казаться, что все почти идеально. Проверяющий
-подскажет тебе, где ты что-то пропустил из-за
-недостатка опыта, что позволит тебе улучшить код.
-
----
-
->Любое знание стоит воспринимать как подобие семантического дерева: убедитесь в том, что понимаете фундаментальные принципы, то есть ствол и крупные ветки, прежде чем лезть в мелкие листья-детали. Иначе последним не на чем будет держаться
-— Илон Маск
-
-Обычно в занятии дается много дополнительного
-материала и ссылок. Не стоит стремиться прочитать
-все ссылки урока, их можно использовать как
-справочник. Гораздо важнее пройти основной
-материал урока и сделать домашнее
-задание - этого достаточно для усвоения
-материала и получения той самой
-основы - ствола и крупных веток, на
-которых впоследствии можно наращивать листву.
-
-
-Как правило, подбираются участники разного
-уровня. Поэтому главное – не стеснятся
-задавать вопросы (после самостоятельного
-гугления и поиска решения). Всегда есть
-поддержка группы (в том числе от пришедших
-на бесплатный повтор участников), моя и ассистентов.
-
----
-### Основные навыки программиста, которые необходимо развить на курсе
-Давайте перечислим набор навыков, которые
-вам необходимо развивать в ходе курса
-и которые необходимы любому программисту:
-
-- умение и привычка искать
-информацию, чтобы иметь больший выбор из
-доступных вариантов технологий и подходов
-к решению задачи, умение пользоваться StackOverflow;
-- умение пользоваться дебаггером в Intellij Idea;
-- умение пользоваться DevTools в браузере;
-- определенный кругозор и опыт для того, чтобы придумывать поисковые запросы.
diff --git a/doc/video4.md b/doc/video4.md
deleted file mode 100644
index 7d9b00716dca..000000000000
--- a/doc/video4.md
+++ /dev/null
@@ -1,110 +0,0 @@
-## Структура приложения (многоуровневая архитектура)
-##  [Видео](https://drive.google.com/file/d/1UHzSy9i-uonmTMFoR5v69Y-vyWLCLQWd)
-
-Приложение, которое мы будем разрабатывать, это [программа для подсчета калорий](http://javaops-demo.ru/topjava).
-
-В этом видео обсудим структуру этого приложения.
-
----
-Ссылки на отчеты, которые будут использоваться в этом уроке:
-
-- [Многоуровневая архитектура (русскоязыная статья в Wikipedia)](https://ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%BE%D0%B3%D0%BE%D1%83%D1%80%D0%BE%D0%B2%D0%BD%D0%B5%D0%B2%D0%B0%D1%8F_%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0)
-- [Multitier architecture (англоязычная статья в Wikipedia)](https://en.wikipedia.org/wiki/Multitier_architecture)
-
----
-
-
-
-На структурной схеме приложения вы видите, что оно условно разделено на 4 части: **Views**,
-**Controller**, **Service** и **Repository**.
-
-Такой подход является реализацией многоуровневой архитектуры в программировании.
-По-английски этот подход называется **_Multitier architecture_**.
-
-Его суть заключается в разделении приложения на несколько слоев,
-каждый из которых ответственен за конкретную задачу.
-
-### Слой отображения (View)
-Views соответствует слою отображения (или presentation layer). Это user interface (UI) или
-фронтенд - все то, что мы видим и с чем взаимодействуем в браузере.
-В качестве View могут быть HTML-страницы, созданные с использованием специальных
-движков шаблонов, например, JSP (встроен в Tomcat), или
-Thymeleaf (шаблоны по умолчанию в Spring Boot), или отдельное frontend-приложение,
-написанное на одном из JavaScript-фреймворков.
-
-В случае с движками шаблонов HTML-страницы будут располагаться в одном проекте
-с основным кодом приложения. Для приложений с "небогатым" UI используются именно шаблоны.
-Этот подход отличается от так называемых RIA - rich internet application - приложений со сложным UI.
-
-Главное отличие rich internet application от приложений с фронтендом
-на движках шаблонов заключается в том, что фронтенд, созданный на движке
-шаблонов, работает на сервере, и это что-то простое.
-В случае с rich internet application фронтенд-приложение загружается
-через Интернет к вам на компьютер и запускается в браузере.
-Оно может быть максимально сложным и выполнять функции традиционных
-десктоп-приложений.
-Иногда в одном приложении смешиваются оба способа: например, страница
-логина-пароля в RIA делаются на шаблонах.
-
-Создание простого фронтенда на движке шаблонов проще, поэтому
-в курсе мы будем использовать этот способ.
-
-### Слой контроллеров (Controller)
-Следующий слой, который мы видим - это **Controller**.
-
-В многоуровневой архитектуре он соответствует слою, который
-называется "**_Слой приложения_**" или "**_Application layer_**").
-В GRASP (General Responsibility Assignment Software Patterns)
-он так и называется - Controller layer.
-
-Это слой приложения, который ответственен за обработку HTTP-запросов и проверку корректности входных данных. Если мы открываем главную страницу на сайте или отправляем заполненную на сайте форму, фронтенд-приложение отправляет HTTP-запрос серверу (в нашем случае контейнеру сервлетов), который принимает запрос и перенаправляет его в контроллер, соответствующий введенному в браузере URL или адресу, который вызывается при отправке формы через сайт.
-Также контроллеры могут принимать запросы не только от фронтенда, но и от других приложений.
-
-Слой контроллеров не имеет доступа к базе данных. Контроллеры общаются только с сервисами.
-
-### Слой сервисов (Service layer)
-Слой **Service** на схеме приложения соответствует слою
-бизнес-логики (или **_Business layer_**) в многоуровневой архитектуре.
-В слое Service инкапсулирована вся бизнес-логика нашего приложения.
-Если коммуникация с фронтендом или другими приложениями - это ответственность контроллеров,
-то обработка данных - это ответственность сервисов.
-
-### Слой доступа к данным (Data layer)
-Слой сервисов общается со слоем, ответственным за работу с базами данных.
-Этот слой называют **Data layer** (также можно встретить
-названия **Persistence Layer** или **Data access layer**), и он
-представлен в виде **_Data Access Object_** классов
-(коротко - **_DAO-классы_**) или классов, реализующих паттерн "репозиторий".
-С обоими видами классов вы попрактикуетесь в ходе курса.
-[Репозиторий - это также один из архитектурных паттернов](https://martinfowler.com/eaaCatalog/repository.html)
-
-Подобное разделение приложения на слои дает гибкость
-и существенно упрощает доработку и переиспользование приложения.
-Например, создав по такому принципу приложение,
-содержащее слои репозиториев, сервисов и контроллеров,
-мы в дальнейшем можем легко использовать это приложение
-с различными фронтенд-приложениями или мобильными приложениями.
-Если мы решим перейти на другую базу данных, мы можем
-переписать только слой репозиториев, и нам не требуется
-вносить изменения в слои сервисов и контроллеров.
-
->Для маленького приложение такой подход может показаться
->избыточно сложным, но по мере расширения это является спасением.
-
-Подавляющее большинство реальных приложений построено с использованием именно этой архитектуры.
-
-### Краткие итоги
-В этом видео мы познакомились с концепцией многоуровневой архитектуры,
-которую мы применим при создании приложения на курсе.
-Многоуровневая архитектура предполагает разделение приложения на слои:
-- Views - слой отображения является фронтендом;
-- Controller - слой приложения ответственен за прием и валидацию входных данных;
-- слой Service включает в себя весь код, отражающий бизнес логику;
-- Repository или Data layer отвечает за взаимодействие с базой данных.
-
-Также мы обсудили какие преимущества дает такой подход.
-Среди преимуществ в первую очередь возможность повторного
-использования различных слоев и упрощение их доработки и изменения.
-
-
-
diff --git a/doc/video5-vcs-git.md b/doc/video5-vcs-git.md
deleted file mode 100644
index 7a172e517bc8..000000000000
--- a/doc/video5-vcs-git.md
+++ /dev/null
@@ -1,180 +0,0 @@
-## Системы управления версиями, Git
-##  [Видео](https://drive.google.com/file/d/1uFjIsxsaSAXxFSwSpjJIGK7Ug2VXf6yH)
-
-video5-vcs-git.md
-
-В этом уроке мы рассмотрим системы управления версиями
-и самую популярную из них - Git.
-
----
-* [StackOverflow 2021 survey](https://insights.stackoverflow.com/survey/2021#technology-most-popular-technologies)
-* [Введение в Git и GitHub: установка и настройка](https://topjava.ru/blog/vvedeniye-v-git-github-ustanovka-i-nastroyka)
-* [Введение в Git и GitHub: базовые команды](https://topjava.ru/blog/vvedeniye-v-git-github-bazovyye-komandy)
-* [Введение в Git и GitHub: ошибки использования](https://topjava.ru/blog/vvedeniye-v-git-oshibki-ispolzovaniya-ch-9)
-* [Бесплатная русскоязычная книга Pro Git](https://git-scm.com/book/ru/v2/)
----
-
-Git является де-факто стандартом среди систем управления версиями.
-Опрос, проведенный StackOverflow в 2021 году показывает,
-что Git используют почти 95% опрошенных разработчиков.
-
-Когда-то популярная централизованная система контроля
-версий SVN (Subversion) практически полностью заменена Git.
-Но некоторые большие проекты все еще используют SVN.
-Примером такого проекта является WordPress.
-Я также все еще иногда встречаю SVN в вакансиях российских компаний.
-
-### Что такое Git и GitHub
-В ходе курса мы будем использовать Git.
-Git - это распределенная система управления версиями.
-Это означает, что код, над которым работает команда,
-и ранее сохраненные (закоммиченные) версии проекта
-хранится на компьютерах каждого члена команды,
-а также в удаленном репозитории, который можно
-сравнить с облачным хранилищем.
-
-Некоторые путают Git и GitHub, но это не одно и то же.
-
-**Git** — это утилита, которую
-программист устанавливает у себя на компьютере для
-сохранения состояний проектов и контроля версий проекта.
-
-**GitHub** — это провайдер удаленных репозиториев,
-сайт (хостинг) для хранения кода проекта и его изменений,
-для обмена файлами с членами команды проекта.
-Программисты могут создавать на GitHub публичные репозитории,
-в которых код доступен всем. Компании могут приобрести платный
-аккаунт на GitHub и вести свои проекты в закрытых репозиториях,
-доступ к которым имеют только члены команды проекта.
-Существуют и другие подобные сайты - провайдеры удаленных
-репозиториев, например, BitBucket, SourceForge, GitLab и т. д.
-
-
-### Как работают Git и GitHub
-
-
-Говоря кратко, работа с Git и удаленным репозиторием
-(в нашем случае это Github) выглядит следующим образом.
-
-Первый разработчик, который начинал работу над проектом,
-использовал команду **_git init_** в корневой директории
-проекта для того, чтобы инициализировать пустой Git-репозиторий.
-В этот момент Git создает в директории с проектом скрытую
-директорию, содержащую файлы, необходимые для его работы.
-Теперь первый разработчик может с помощью команды **_git add_**
-добавлять файлы проекта в индекс - то есть в зону,
-отслеживаемую git, а также может фиксировать изменения,
-создавая коммиты (**_git commit_**), и загружать их в
-удаленный репозиторий (например, на GitHub) с помощью
-команды **_git push_**.
-
-Спустя некоторое время к работе над проектом подключается второй разработчик.
-
-Поскольку первый разработчик пушил изменения файлов проекта
-на удаленный репозиторий, второй разработчик может скачать
-их себе на компьютер с помощью команды **_git pull_**
-и продолжить работу с учетом этих изменений.
-
-Давайте повторим.
-В описанной работе использовались команды:
-
-* **_git init_** - эта команда инициализирует работу Git
-для конкретного проекта. В папке с проектом создается
-скрытая папка, хранящая все файлы, необходимые Git
-для работы. Папка с этими файлами впоследствии будет
-загружена в удаленный репозиторий, и второй программист,
-скачавший проект, увидит у себя на компьютере все ранее
-созданные с помощью Git версии.
-* **_git add_** - это команда, которая добавляет файлы
-проекта в индекс (или отслеживаемую зону), чтобы их можно
-было в какой-то момент закоммитить (то есть сохранить, зафиксировать).
-* **_git commit_** - сохраняет все файлы проекта в
-текущем состоянии так, что мы в любой момент сможем
-вернуться к этому состоянию - посмотреть, как изменились
-файлы, или даже полностью вернуть проект к состоянию
-данного коммита.
-* **_git push_** - загружает файлы в удаленный Git-репозиторий,
-чтобы их могли скачать другие программисты.
-* **_git pull_** используют для того, чтобы скачать
-файлы из удаленного репозитория себе на компьютер.
-
-Также Git позволяет создавать ветки. Это означает,
-что вы можете создать копию стабильного кода и
-продолжить работу в этой копии, не подвергая риску
-стабильную версию. Когда код с новой функциональностью
-готов, отлажен и протестирован, он может быть
-слит ("смержен", от слова "merge") в основную ветку со стабильным кодом.
-
-С помощью этих функций Git позволяет команде разработчиков
-работать одновременно над разными задачами и поддерживать
-код в стабильном состоянии.
-
-Также Git позволяет в любой момент понять, кто и когда внес
-изменения в определенный код, что существенно упрощает взаимодействия
-с коллегами. Например, в случае, если вы видите новые
-строки кода, которые вызывают у вас вопросы, с помощью
-Git вы можете разобраться, кто внес изменения именно
-в эти строки, и связаться с этим человеком
-для уточнения деталей.
-
-Если произошла ситуация, при которой два разработчика
-изменили один и тот же файл, Git потребует от вас вручную
-отрегулировать такой конфликт.
-
-Также благодаря Git у вас всегда под рукой детальная
-история изменений. С помощью комментариев к коммитам
-вы можете детально документировать ход работы над
-проектом и в случае необходимости разобраться, в
-какой момент что-то пошло не так, и откатиться
-к той версии, где ошибка еще не была сделана.
-Или просто выяснить, как выглядел код определенного
-класса или метода в определенной версии и внести правки
-в текущую версию, если это необходимо.
-
-### Дополнительные материалы
-
-Для изучения Git рекомендуем в первую очередь ознакомиться со статьями на сайте [topjava.ru](https://topjava.ru):
-
-* [Введение в Git и GitHub: установка и настройка](https://topjava.ru/blog/vvedeniye-v-git-github-ustanovka-i-nastroyka)
-* [Введение в Git и GitHub: базовые команды](https://topjava.ru/blog/vvedeniye-v-git-github-bazovyye-komandy)
-* [Введение в Git и GitHub: ошибки использования](https://topjava.ru/blog/vvedeniye-v-git-oshibki-ispolzovaniya-ch-9)
-
-В них дается детальная инструкция по первоначальной настройке
-Git, GitHub, в том числе инструкция по настройке
-Access token в GitHub, что вам обязательно нужно будет сделать.
-
-Также можно упомянуть официальную русскоязычную книгу Pro Git,
-которая доступна [бесплатно в электронном виде](https://git-scm.com/book/ru/v2/).
-
-### Как мы будем использовать Git на курсе
-В Git очень много команд, но в ходе курса мы будем использовать
-только самые необходимые из них.
-С git можно работать как через терминал с помощью команд,
-так и с помощью различных программ с графическим интерфейсом.
-Мы будем преимущественно использовать интеграцию
-IntelliJ Idea с Git, поскольку она очень удобна
-и существенно упрощает работу.
-Однако знать, как выполнять аналогичные операции
-в терминале, очень полезно.
-
-### Резюме
-В этом уроке мы сделали краткий обзор систем управления
-версиями и самой популярной из них - Git:
-
-* разобрались в различиях Git и GitHub.
-Git - это система контроля версий, а
-GitHub - провайдер удаленных репозиториев, один из многих, доступных на рынке.
-* кратко познакомились с тем, как Git работает.
-Узнали о командах git init, git add, git commit, git push и git pull.
-* Также мы обсудили, что такое ветки и какие возможности они дают.
-
----
-
-Если вы не работали раньше с Git, не беспокойтесь.
-В последующих уроках работа с Git будет изучаться
-на практике в ходе работы над проектом. Но дополнительно
-почитать о Git все же стоит.
-
-
-
-
diff --git a/pom.xml b/pom.xml
index 0b1c2896da5b..65b183c52b30 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,41 +4,401 @@
ru.javawebinartopjava
- jar
+ war1.0-SNAPSHOTCalories Management
- http://topjava.herokuapp.com/
+ https://javaops-demo.ru/topjava
- 1.8
+ 21UTF-8UTF-8
+ 5.3.39
+ 5.8.16
+ 2.7.18
+ 2.18.1
+ 9.0.96
+
+
+ 5.6.15.Final
+ 6.2.5.Final
+ 3.0.1-b12
+
+
+ 3.10.8
+
+
+ 4.6.2
+ 3.7.1
+ 2.5.20-1
+ 3.1.4
+ 1.13.5
+
+
+ 1.5.12
+ 2.0.16
+
+
+ 42.7.4
+
+ 5.11.3
+ 3.26.3
+ 3.0topjava
- install
+ package
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.4.0
+ org.apache.maven.pluginsmaven-compiler-plugin
- 3.8.1
+ 3.13.0${java.version}${java.version}
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.1
+
+ -Dfile.encoding=UTF-8
+
+
+
+
+
+
+ org.codehaus.cargo
+ cargo-maven3-plugin
+ 1.10.15
+
+
+ tomcat9x
+
+ UTF-8
+ tomcat,datajpa
+
+
+
+ org.postgresql
+ postgresql
+
+
+
+
+
+
+ src/main/resources/tomcat/context.xml
+ conf/Catalina/localhost/
+ ${project.build.finalName}.xml
+
+
+
+
+
+ ru.javawebinar
+ topjava
+ war
+
+ ${project.build.finalName}
+
+
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ runtime
+
+
+
+
+ com.google.code.findbugs
+ annotations
+ 3.0.1
+ compile
+
+
+
+ javax.annotation
+ javax.annotation-api
+ 1.3.2
+
+
+
+
+ org.springframework
+ spring-context-support
+
+
+ org.springframework.data
+ spring-data-jpa
+ ${spring-data-jpa.version}
+
+
+
+
+ org.springframework.security
+ spring-security-web
+ ${spring.security.version}
+
+
+ org.springframework.security
+ spring-security-config
+ ${spring.security.version}
+
+
+
+
+ org.hibernate
+ hibernate-core
+ ${hibernate.version}
+
+
+ org.hibernate.validator
+ hibernate-validator
+ ${hibernate-validator.version}
+
+
+ org.hibernate
+ hibernate-jcache
+ ${hibernate.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
+
+
+
+
+ org.apache.tomcat
+ tomcat-servlet-api
+ ${tomcat.version}
+ provided
+
+
+
+ javax.servlet
+ jstl
+ 1.2
+
+
+
+ org.springframework
+ spring-webmvc
+
+
+
+
+ org.webjars
+ jquery
+ ${webjars-jquery.version}
+
+
+ org.webjars
+ bootstrap
+ ${webjars-bootstrap.version}
+
+
+ org.webjars
+ jquery
+
+
+
+ org.webjars
+ popper.js
+
+
+
+
+ org.webjars
+ datatables
+ ${webjars-datatables.version}
+
+
+ org.webjars
+ jquery
+
+
+
+
+ org.webjars
+ datetimepicker
+ ${webjars-datetimepicker.version}
+
+
+ org.webjars.bower
+ noty
+ ${webjars-noty.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-hibernate5
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ ${jackson.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.jupiter.version}
+ test
+
+
+ org.hamcrest
+ hamcrest-core
+ ${hamcrest.version}
+ test
+
+
+
+ org.springframework
+ spring-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ ${spring.security.version}
+ test
+
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+
+
+ org.junit.platform
+ junit-platform-launcher
+ 1.11.3
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.8.1
+ test
+
+
+ hsqldb
+
+
+ org.hsqldb
+ hsqldb
+ 2.7.4
+
+
+
+
+
+ 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
+
+
-
+
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java
new file mode 100644
index 000000000000..47c06302d022
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java
@@ -0,0 +1,36 @@
+package ru.javawebinar.topjava;
+
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.UsersUtil;
+
+import java.io.Serial;
+
+public class AuthorizedUser extends org.springframework.security.core.userdetails.User {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private UserTo userTo;
+
+ public AuthorizedUser(User user) {
+ super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles());
+ this.userTo = UsersUtil.asTo(user);
+ }
+
+ public int getId() {
+ return userTo.id();
+ }
+
+ public void update(UserTo newTo) {
+ userTo = newTo;
+ }
+
+ public UserTo getUserTo() {
+ return userTo;
+ }
+
+ @Override
+ public String toString() {
+ return userTo.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/HasId.java b/src/main/java/ru/javawebinar/topjava/HasId.java
new file mode 100644
index 000000000000..2ba30235cb87
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/HasId.java
@@ -0,0 +1,19 @@
+package ru.javawebinar.topjava;
+
+import org.springframework.util.Assert;
+
+public interface HasId {
+ Integer getId();
+
+ void setId(Integer id);
+
+ default boolean isNew() {
+ return getId() == null;
+ }
+
+ // doesn't work for hibernate lazy proxy
+ default int id() {
+ Assert.notNull(getId(), "Entity must has id");
+ return getId();
+ }
+}
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..e4111a2afd28
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/Profiles.java
@@ -0,0 +1,27 @@
+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";
+
+ // 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/View.java b/src/main/java/ru/javawebinar/topjava/View.java
new file mode 100644
index 000000000000..5bf0ef1aba8b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/View.java
@@ -0,0 +1,6 @@
+package ru.javawebinar.topjava;
+
+public class View {
+ public interface JsonREST {}
+ public interface JsonUI {}
+}
\ No newline at end of file
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..16ae37b0fa73
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java
@@ -0,0 +1,58 @@
+package ru.javawebinar.topjava.model;
+
+import ru.javawebinar.topjava.HasId;
+
+import javax.persistence.*;
+
+import static org.hibernate.proxy.HibernateProxyHelper.getClassWithoutInitializingProxy;
+
+@MappedSuperclass
+// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access
+@Access(AccessType.FIELD)
+//@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE)
+public abstract class AbstractBaseEntity implements HasId {
+ public static final int START_SEQ = 100000;
+
+ @Id
+ @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ)
+ // @Column(name = "id", unique = true, nullable = false, columnDefinition = "integer default nextval('global_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;
+ }
+
+ @Override
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ @Override
+ public Integer getId() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ":" + getId();
+ }
+
+ // https://stackoverflow.com/a/78077907/548473
+ @Override
+ public final boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClassWithoutInitializingProxy(this) != getClassWithoutInitializingProxy(o)) return false;
+ return getId() != null && getId().equals(((AbstractBaseEntity) o).getId());
+ }
+
+ @Override
+ public final int hashCode() {
+ return getClassWithoutInitializingProxy(this).hashCode();
+ }
+}
\ 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..0b32aac584f5
--- /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 = 128)
+ @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..73e243cb1185
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java
@@ -0,0 +1,133 @@
+package ru.javawebinar.topjava.model;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonView;
+import org.hibernate.annotations.OnDelete;
+import org.hibernate.annotations.OnDeleteAction;
+import org.hibernate.validator.constraints.Range;
+import org.springframework.format.annotation.DateTimeFormat;
+import ru.javawebinar.topjava.View;
+import ru.javawebinar.topjava.util.DateTimeUtil;
+
+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.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 = "meal", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meal_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
+ @JsonView(View.JsonREST.class)
+ private LocalDateTime dateTime;
+
+ @Column(name = "description", nullable = false)
+ @NotBlank
+ @Size(min = 2, max = 120)
+ private String description;
+
+ @Column(name = "calories", nullable = false)
+ @NotNull
+ @Range(min = 10, max = 5000)
+ private Integer calories;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ @JsonBackReference
+// @NotNull
+ private User user;
+
+ public Meal() {
+ }
+
+ public Meal(Integer id, LocalDateTime dateTime, String description, int calories) {
+ super(id);
+ this.dateTime = dateTime;
+ this.description = description;
+ this.calories = calories;
+ }
+
+ public LocalDateTime getDateTime() {
+ return dateTime;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public LocalDate getDate() {
+ return dateTime.toLocalDate();
+ }
+
+ public LocalTime getTime() {
+ return dateTime.toLocalTime();
+ }
+
+ public void setDateTime(LocalDateTime dateTime) {
+ this.dateTime = dateTime;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public void setCalories(Integer calories) {
+ this.calories = calories;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+
+ @JsonGetter
+ @JsonView(View.JsonUI.class)
+ @JsonFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN)
+ public LocalDateTime getDateTimeUI() {
+ return dateTime;
+ }
+
+ @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN)
+ public void setDateTimeUI(LocalDateTime dateTime) {
+ this.dateTime = dateTime;
+ }
+
+ @Override
+ public String toString() {
+ return "Meal{" +
+ "id=" + id +
+ ", dateTime=" + dateTime +
+ ", description='" + description + '\'' +
+ ", calories=" + calories +
+ '}';
+ }
+}
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..27f3e52311f8
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/Role.java
@@ -0,0 +1,14 @@
+package ru.javawebinar.topjava.model;
+
+import org.springframework.security.core.GrantedAuthority;
+
+public enum Role implements GrantedAuthority {
+ USER,
+ ADMIN;
+
+// https://stackoverflow.com/a/19542316/548473
+ @Override
+ public String getAuthority() {
+ return "ROLE_" + name();
+ }
+}
\ 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..7880f2d7f0b4
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/User.java
@@ -0,0 +1,165 @@
+package ru.javawebinar.topjava.model;
+
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+import org.hibernate.annotations.Cache;
+import org.hibernate.annotations.*;
+import org.hibernate.validator.constraints.Range;
+import org.springframework.util.CollectionUtils;
+
+import javax.persistence.Entity;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OrderBy;
+import javax.persistence.Table;
+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.UsersUtil.DEFAULT_CALORIES_PER_DAY;
+
+@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
+@NamedQueries({
+ @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"),
+ @NamedQuery(name = User.BY_EMAIL, query = "SELECT DISTINCT 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 = 128)
+ private String email;
+
+ @Column(name = "password", nullable = false)
+ @NotBlank
+ @Size(min = 5, max = 128)
+ 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()", updatable = false)
+ @NotNull
+ private Date registered = new Date();
+
+ @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
+ @Enumerated(EnumType.STRING)
+ @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"),
+ uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role")})
+ @Column(name = "role")
+ @ElementCollection(fetch = FetchType.EAGER)
+ // @Fetch(FetchMode.SUBSELECT)
+ @BatchSize(size = 200)
+ @JoinColumn
+ @OnDelete(action = OnDeleteAction.CASCADE)
+ 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")//, cascade = CascadeType.REMOVE, orphanRemoval = true)
+ @OrderBy("dateTime DESC")
+ @OnDelete(action = OnDeleteAction.CASCADE) //https://stackoverflow.com/a/44988100/548473
+ @JsonManagedReference
+ 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... roles) {
+ this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), List.of(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 void setRoles(Collection roles) {
+ this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles);
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public List getMeals() {
+ return meals;
+ }
+
+ public void setMeals(List meals) {
+ this.meals = 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..1ad7f8d943d7
--- /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 does not belong to userId
+ Meal save(Meal meal, int userId);
+
+ // false if meal does not belong to userId
+ boolean delete(int id, int userId);
+
+ // null if meal does 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..f3f362bdfaf0
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java
@@ -0,0 +1,27 @@
+package ru.javawebinar.topjava.repository.datajpa;
+
+import org.springframework.data.jpa.repository.*;
+import org.springframework.data.repository.query.Param;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.model.User;
+
+import javax.persistence.QueryHint;
+
+@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);
+
+ // https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct
+ @QueryHints({
+ @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false")
+ })
+ User getByEmail(String email);
+
+ // https://stackoverflow.com/a/46013654/548473
+ @EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD)
+ @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..b5f4e3eebfdd
--- /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.id(), userId) == null) {
+ return null;
+ }
+ meal.setUser(crudUserRepository.getReferenceById(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..d4eb271cad84
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java
@@ -0,0 +1,50 @@
+package ru.javawebinar.topjava.repository.datajpa;
+
+import org.springframework.beans.factory.annotation.Autowired;
+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/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java
new file mode 100644
index 000000000000..775d314ed961
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java
@@ -0,0 +1,91 @@
+package ru.javawebinar.topjava.repository.jdbc;
+
+import org.springframework.dao.support.DataAccessUtils;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+import ru.javawebinar.topjava.util.ValidationUtil;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Repository
+@Transactional(readOnly = true)
+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("meal")
+ .usingGeneratedKeyColumns("id");
+
+ this.jdbcTemplate = jdbcTemplate;
+ this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
+ }
+
+ @Override
+ @Transactional
+ public Meal save(Meal meal, int userId) {
+ ValidationUtil.validate(meal);
+
+ 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 meal " +
+ " SET description=:description, calories=:calories, date_time=:date_time " +
+ " WHERE id=:id AND user_id=:user_id", map) == 0) {
+ return null;
+ }
+ }
+ return meal;
+ }
+
+ @Override
+ @Transactional
+ public boolean delete(int id, int userId) {
+ return jdbcTemplate.update("DELETE FROM meal WHERE id=? AND user_id=?", id, userId) != 0;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ List meals = jdbcTemplate.query(
+ "SELECT * FROM meal WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId);
+ return DataAccessUtils.singleResult(meals);
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return jdbcTemplate.query(
+ "SELECT * FROM meal 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 meal 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..ca5dcc185cd8
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java
@@ -0,0 +1,123 @@
+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 org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+import ru.javawebinar.topjava.util.ValidationUtil;
+
+import java.util.*;
+
+@Repository
+@Transactional(readOnly = true)
+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
+ @Transactional
+ public User save(User user) {
+ ValidationUtil.validate(user);
+
+ BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user);
+
+ if (user.isNew()) {
+ Number newKey = insertUser.executeAndReturnKey(parameterSource);
+ user.setId(newKey.intValue());
+ insertRoles(user);
+ } 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;
+ }
+ // Simplest implementation.
+ // More complicated : get user roles from DB and compare them with user.roles (assume that roles are changed rarely).
+ // If roles are changed, calculate difference in java and delete/insert them.
+ deleteRoles(user);
+ insertRoles(user);
+ }
+ return user;
+ }
+
+ @Override
+ @Transactional
+ public boolean delete(int id) {
+ return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0;
+ }
+
+ @Override
+ public User get(int id) {
+ List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id);
+ return setRoles(DataAccessUtils.singleResult(users));
+ }
+
+ @Override
+ public User getByEmail(String email) {
+// return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email);
+ List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email);
+ return setRoles(DataAccessUtils.singleResult(users));
+ }
+
+ @Override
+ public List getAll() {
+ List users = jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER);
+
+ Map> map = new HashMap<>();
+ jdbcTemplate.query("SELECT * FROM user_role", rs -> {
+ map.computeIfAbsent(rs.getInt("user_id"), userId -> EnumSet.noneOf(Role.class))
+ .add(Role.valueOf(rs.getString("role")));
+ });
+ users.forEach(u -> u.setRoles(map.get(u.getId())));
+ return users;
+ }
+
+ private void insertRoles(User u) {
+ Set roles = u.getRoles();
+ if (!CollectionUtils.isEmpty(roles)) {
+ jdbcTemplate.batchUpdate("INSERT INTO user_role (user_id, role) VALUES (?, ?)", roles, roles.size(),
+ (ps, role) -> {
+ ps.setInt(1, u.id());
+ ps.setString(2, role.name());
+ });
+ }
+ }
+
+ private void deleteRoles(User u) {
+ jdbcTemplate.update("DELETE FROM user_role WHERE user_id=?", u.getId());
+ }
+
+ private User setRoles(User u) {
+ if (u != null) {
+ List roles = jdbcTemplate.queryForList("SELECT role FROM user_role WHERE user_id=?", Role.class, u.getId());
+ u.setRoles(roles);
+ }
+ return u;
+ }
+}
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..6df9fd99f575
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java
@@ -0,0 +1,62 @@
+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;
+ }
+ return get(meal.id(), userId) == null ? null : 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..22fa8f4e35e1
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java
@@ -0,0 +1,75 @@
+package ru.javawebinar.topjava.repository.jpa;
+
+import org.hibernate.jpa.QueryHints;
+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)
+ .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
+ .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..fb79bb47c800
--- /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.checkNotFound;
+
+@Service
+public class MealService {
+
+ private final MealRepository repository;
+
+ public MealService(MealRepository repository) {
+ this.repository = repository;
+ }
+
+ public Meal get(int id, int userId) {
+ return checkNotFound(repository.get(id, userId), id);
+ }
+
+ public void delete(int id, int userId) {
+ checkNotFound(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");
+ checkNotFound(repository.save(meal, userId), meal.getId());
+ }
+
+ 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 checkNotFound(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..8459f4e8cc32
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java
@@ -0,0 +1,93 @@
+package ru.javawebinar.topjava.service;
+
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.context.annotation.Scope;
+import org.springframework.context.annotation.ScopedProxyMode;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.AuthorizedUser;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.UsersUtil;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound;
+
+@Service("userService")
+@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
+public class UserService implements UserDetailsService {
+
+ 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) {
+ checkNotFound(repository.delete(id), id);
+ }
+
+ public User get(int id) {
+ return checkNotFound(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");
+// checkNotFound : check works only for JDBC, disabled
+ repository.save(user);
+ }
+
+
+ @CacheEvict(value = "users", allEntries = true)
+ @Transactional
+ public void update(UserTo userTo) {
+ User user = get(userTo.id());
+ User updatedUser = UsersUtil.updateFromTo(user, userTo);
+ repository.save(updatedUser); // !! need only for JDBC implementation
+ }
+
+ @CacheEvict(value = "users", allEntries = true)
+ @Transactional
+ public void enable(int id, boolean enabled) {
+ User user = get(id);
+ user.setEnabled(enabled);
+ repository.save(user); // !! need only for JDBC implementation
+ }
+
+ @Override
+ public AuthorizedUser loadUserByUsername(String email) throws UsernameNotFoundException {
+ User user = repository.getByEmail(email.toLowerCase());
+ if (user == null) {
+ throw new UsernameNotFoundException("User " + email + " is not found");
+ }
+ return new AuthorizedUser(user);
+ }
+
+ public User getWithMeals(int id) {
+ return checkNotFound(repository.getWithMeals(id), id);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/to/BaseTo.java b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java
new file mode 100644
index 000000000000..b7a7de6b7d66
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java
@@ -0,0 +1,24 @@
+package ru.javawebinar.topjava.to;
+
+import ru.javawebinar.topjava.HasId;
+
+public abstract class BaseTo implements HasId {
+ protected Integer id;
+
+ public BaseTo() {
+ }
+
+ public BaseTo(Integer id) {
+ this.id = id;
+ }
+
+ @Override
+ public Integer getId() {
+ return id;
+ }
+
+ @Override
+ public void setId(Integer id) {
+ this.id = id;
+ }
+}
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..526f26e40e89
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java
@@ -0,0 +1,83 @@
+package ru.javawebinar.topjava.to;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonView;
+import ru.javawebinar.topjava.View;
+import ru.javawebinar.topjava.util.DateTimeUtil;
+
+import java.beans.ConstructorProperties;
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+public class MealTo extends BaseTo {
+
+ @JsonView(View.JsonREST.class)
+ private final LocalDateTime dateTime;
+
+ private final String description;
+
+ private final int calories;
+
+ private final boolean excess;
+
+ @ConstructorProperties({"id", "dateTime", "description", "calories", "excess"})
+ public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) {
+ super(id);
+ this.dateTime = dateTime;
+ this.description = description;
+ this.calories = calories;
+ this.excess = excess;
+ }
+
+ public LocalDateTime getDateTime() {
+ return dateTime;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public boolean isExcess() {
+ return excess;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ MealTo mealTo = (MealTo) o;
+ return calories == mealTo.calories &&
+ excess == mealTo.excess &&
+ Objects.equals(id, mealTo.id) &&
+ Objects.equals(dateTime, mealTo.dateTime) &&
+ Objects.equals(description, mealTo.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, dateTime, description, calories, excess);
+ }
+
+ @JsonGetter
+ @JsonView(View.JsonUI.class)
+ @JsonFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN)
+ public LocalDateTime getDateTimeUI() {
+ return dateTime;
+ }
+
+ @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..3a421df497fb
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java
@@ -0,0 +1,82 @@
+package ru.javawebinar.topjava.to;
+
+import org.hibernate.validator.constraints.Range;
+import ru.javawebinar.topjava.util.UsersUtil;
+
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.io.Serial;
+import java.io.Serializable;
+
+public class UserTo extends BaseTo implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @NotBlank
+ @Size(min = 2, max = 100)
+ private String name;
+
+ @Email
+ @NotBlank
+ @Size(max = 100)
+ private String email;
+
+ @NotBlank
+ @Size(min = 5, max = 32, message = "length must be between 5 and 32 characters")
+ private String password;
+
+ @Range(min = 10, max = 10000)
+ @NotNull
+ private Integer caloriesPerDay = UsersUtil.DEFAULT_CALORIES_PER_DAY;
+
+ public UserTo() {
+ }
+
+ public UserTo(Integer id, String name, String email, String password, int caloriesPerDay) {
+ super(id);
+ this.name = name;
+ this.email = email;
+ this.password = password;
+ this.caloriesPerDay = caloriesPerDay;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public Integer getCaloriesPerDay() {
+ return caloriesPerDay;
+ }
+
+ @Override
+ public String toString() {
+ return "UserTo{" +
+ "id=" + id +
+ ", name='" + name + '\'' +
+ ", email='" + email + '\'' +
+ ", caloriesPerDay='" + caloriesPerDay + '\'' +
+ '}';
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
new file mode 100644
index 000000000000..5ad95caf429c
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
@@ -0,0 +1,48 @@
+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;
+
+public class DateTimeUtil {
+ public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm";
+ public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
+
+ public static boolean isBetweenHalfOpen(LocalTime lt, LocalTime startTime, LocalTime endTime) {
+ return !lt.isBefore(startTime) && lt.isBefore(endTime);
+ }
+
+ // 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.plusDays(1).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..af07e74d3ec7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java
@@ -0,0 +1,43 @@
+package ru.javawebinar.topjava.util;
+
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealTo;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+public class MealsUtil {
+
+ 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));
+ }
+
+ private static List filterByPredicate(Collection meals, int caloriesPerDay, Predicate filter) {
+ 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(filter)
+ .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay))
+ .toList();
+ }
+
+ public 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/UsersUtil.java b/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java
new file mode 100644
index 000000000000..8243fa95537b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java
@@ -0,0 +1,26 @@
+package ru.javawebinar.topjava.util;
+
+import ru.javawebinar.topjava.model.Role;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+
+public class UsersUtil {
+
+ public static final int DEFAULT_CALORIES_PER_DAY = 2000;
+
+ public static User createNewFromTo(UserTo userTo) {
+ return new User(null, userTo.getName(), userTo.getEmail().toLowerCase(), userTo.getPassword(), Role.USER);
+ }
+
+ public static UserTo asTo(User user) {
+ return new UserTo(user.getId(), user.getName(), user.getEmail(), user.getPassword(), user.getCaloriesPerDay());
+ }
+
+ public static User updateFromTo(User user, UserTo userTo) {
+ user.setName(userTo.getName());
+ user.setEmail(userTo.getEmail().toLowerCase());
+ user.setCaloriesPerDay(userTo.getCaloriesPerDay());
+ user.setPassword(userTo.getPassword());
+ return user;
+ }
+}
\ No newline at end of file
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..9a083383d04e
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/Util.java
@@ -0,0 +1,13 @@
+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..c5a9e68bf786
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java
@@ -0,0 +1,87 @@
+package ru.javawebinar.topjava.util;
+
+
+import org.springframework.core.NestedExceptionUtils;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.NonNull;
+import org.springframework.validation.BindingResult;
+import ru.javawebinar.topjava.HasId;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import javax.validation.*;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ValidationUtil {
+
+ private static final Validator validator;
+
+ static {
+ // From Javadoc: implementations are thread-safe and instances are typically cached and reused.
+ ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+ // From Javadoc: implementations of this interface must be thread-safe
+ validator = factory.getValidator();
+ }
+
+ private ValidationUtil() {
+ }
+
+ public static void validate(T bean) {
+ // https://alexkosarev.name/2018/07/30/bean-validation-api/
+ Set> violations = validator.validate(bean);
+ if (!violations.isEmpty()) {
+ throw new ConstraintViolationException(violations);
+ }
+ }
+
+
+ public static T checkNotFound(T object, int id) {
+ checkNotFound(object != null, id);
+ return object;
+ }
+
+ public static void checkNotFound(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 checkIsNew(HasId bean) {
+ if (!bean.isNew()) {
+ throw new IllegalArgumentException(bean + " must be new (id=null)");
+ }
+ }
+
+ public static void assureIdConsistent(HasId bean, int id) {
+// conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473)
+ if (bean.isNew()) {
+ bean.setId(id);
+ } else if (bean.id() != id) {
+ throw new IllegalArgumentException(bean + " must be with id=" + id);
+ }
+ }
+
+ // https://stackoverflow.com/a/65442410/548473
+ @NonNull
+ public static Throwable getRootCause(@NonNull Throwable t) {
+ Throwable rootCause = NestedExceptionUtils.getRootCause(t);
+ return rootCause != null ? rootCause : t;
+ }
+
+ public static ResponseEntity getErrorResponse(BindingResult result) {
+ return ResponseEntity.unprocessableEntity().body(
+ result.getFieldErrors().stream()
+ .map(fe -> String.format("[%s] %s", fe.getField(), fe.getDefaultMessage()))
+ .collect(Collectors.joining(" "))
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
new file mode 100644
index 000000000000..f1e9b0e46376
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java
@@ -0,0 +1,7 @@
+package ru.javawebinar.topjava.util.exception;
+
+public class NotFoundException extends RuntimeException {
+ public NotFoundException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java
new file mode 100644
index 000000000000..af0435e78a8e
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java
@@ -0,0 +1,35 @@
+package ru.javawebinar.topjava.web;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+@Controller
+public class RootController {
+ private static final Logger log = LoggerFactory.getLogger(RootController.class);
+
+ @GetMapping("/")
+ public String root() {
+ log.info("root");
+ return "redirect:meals";
+ }
+
+ @GetMapping("/users")
+ public String getUsers() {
+ log.info("users");
+ return "users";
+ }
+
+ @GetMapping("/login")
+ public String login() {
+ log.info("login");
+ return "login";
+ }
+
+ @GetMapping("/meals")
+ public String getMeals() {
+ log.info("meals");
+ return "meals";
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java
new file mode 100644
index 000000000000..ac5b99f052f1
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java
@@ -0,0 +1,34 @@
+package ru.javawebinar.topjava.web;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import ru.javawebinar.topjava.AuthorizedUser;
+
+import static java.util.Objects.requireNonNull;
+
+public class SecurityUtil {
+
+ private SecurityUtil() {
+ }
+
+ public static AuthorizedUser safeGet() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null) {
+ return null;
+ }
+ Object principal = auth.getPrincipal();
+ return (principal instanceof AuthorizedUser) ? (AuthorizedUser) principal : null;
+ }
+
+ public static AuthorizedUser get() {
+ return requireNonNull(safeGet(), "No authorized user found");
+ }
+
+ public static int authUserId() {
+ return get().getUserTo().id();
+ }
+
+ public static int authUserCaloriesPerDay() {
+ return get().getUserTo().getCaloriesPerDay();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java
new file mode 100644
index 000000000000..15448ce8c68a
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java
@@ -0,0 +1,36 @@
+package ru.javawebinar.topjava.web.converter;
+
+import org.springframework.format.Formatter;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+public class DateTimeFormatters {
+ public static class LocalDateFormatter implements Formatter {
+
+ @Override
+ public LocalDate parse(String text, Locale locale) {
+ return LocalDate.parse(text);
+ }
+
+ @Override
+ public String print(LocalDate lt, Locale locale) {
+ return lt.format(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+ }
+
+ public static class LocalTimeFormatter implements Formatter {
+
+ @Override
+ public LocalTime parse(String text, Locale locale) {
+ return LocalTime.parse(text);
+ }
+
+ @Override
+ public String print(LocalTime lt, Locale locale) {
+ return lt.format(DateTimeFormatter.ISO_LOCAL_TIME);
+ }
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java
new file mode 100644
index 000000000000..58704d86db13
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java
@@ -0,0 +1,41 @@
+package ru.javawebinar.topjava.web.json;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import ru.javawebinar.topjava.View;
+
+/**
+ *
+ * Handling Hibernate lazy-loading
+ *
+ * @link https://github.com/FasterXML/jackson
+ * @link https://github.com/FasterXML/jackson-datatype-hibernate
+ * @link https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers
+ */
+public class JacksonObjectMapper extends ObjectMapper {
+
+ private static final ObjectMapper MAPPER = new JacksonObjectMapper();
+
+ private JacksonObjectMapper() {
+ registerModule(new Hibernate5Module());
+
+ registerModule(new JavaTimeModule());
+ configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+
+ setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
+ setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+// https://stackoverflow.com/questions/22875642/jackson-set-default-view
+ setConfig(getSerializationConfig().withView(View.JsonREST.class));
+ }
+
+ public static ObjectMapper getMapper() {
+ return MAPPER;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
new file mode 100644
index 000000000000..99618d333f78
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java
@@ -0,0 +1,46 @@
+package ru.javawebinar.topjava.web.json;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+
+import java.io.IOException;
+import java.util.List;
+
+import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper;
+
+public class JsonUtil {
+
+ public static List readValues(String json, Class clazz) {
+ ObjectReader reader = getMapper().readerFor(clazz);
+ try {
+ return reader.readValues(json).readAll();
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Invalid read array from JSON:\n'" + json + "'", e);
+ }
+ }
+
+ public static T readValue(String json, Class clazz) {
+ try {
+ return getMapper().readValue(json, clazz);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Invalid read from JSON:\n'" + json + "'", e);
+ }
+ }
+
+ public static String writeValue(T obj) {
+ try {
+ return getMapper().writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e);
+ }
+ }
+
+ public static String writeValue(T obj, ObjectWriter ow) {
+ try {
+ return ow.writeValueAsString(obj);
+ } catch (JsonProcessingException e) {
+ throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
new file mode 100644
index 000000000000..336a85f5f6fa
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
@@ -0,0 +1,72 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.Nullable;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.service.MealService;
+import ru.javawebinar.topjava.to.MealTo;
+import ru.javawebinar.topjava.util.MealsUtil;
+import ru.javawebinar.topjava.web.SecurityUtil;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkIsNew;
+
+public abstract class AbstractMealController {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ @Autowired
+ private MealService service;
+
+ public Meal get(int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("get meal {} for user {}", id, userId);
+ return service.get(id, userId);
+ }
+
+ public void delete(int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("delete meal {} for user {}", id, userId);
+ service.delete(id, userId);
+ }
+
+ public List getAll() {
+ int userId = SecurityUtil.authUserId();
+ log.info("getAll for user {}", userId);
+ return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay());
+ }
+
+ public Meal create(Meal meal) {
+ int userId = SecurityUtil.authUserId();
+ log.info("create {} for user {}", meal, userId);
+ checkIsNew(meal);
+ return service.create(meal, userId);
+ }
+
+ public void update(Meal meal, int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("update {} for user {}", meal, userId);
+ assureIdConsistent(meal, id);
+ service.update(meal, userId);
+ }
+
+ /**
+ * Filter separately
+ *
by date
+ *
by time for every date
+ *
+ */
+ public List getBetween(@Nullable LocalDate startDate, @Nullable LocalTime startTime,
+ @Nullable LocalDate endDate, @Nullable LocalTime endTime) {
+ int userId = SecurityUtil.authUserId();
+ log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId);
+
+ List mealsDateFiltered = service.getBetweenInclusive(startDate, endDate, userId);
+ return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
new file mode 100644
index 000000000000..54d827d282f7
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
@@ -0,0 +1,68 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.Nullable;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealTo;
+
+import java.net.URI;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = MealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class MealRestController extends AbstractMealController {
+ static final String REST_URL = "/rest/profile/meals";
+
+ @Override
+ @GetMapping("/{id}")
+ public Meal get(@PathVariable int id) {
+ return super.get(id);
+ }
+
+ @Override
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable int id) {
+ super.delete(id);
+ }
+
+ @Override
+ @GetMapping
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void update(@RequestBody Meal meal, @PathVariable int id) {
+ super.update(meal, id);
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@RequestBody Meal meal) {
+ Meal created = super.create(meal);
+
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL + "/{id}")
+ .buildAndExpand(created.getId()).toUri();
+
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @Override
+ @GetMapping("/filter")
+ public List getBetween(
+ @RequestParam @Nullable LocalDate startDate,
+ @RequestParam @Nullable LocalTime startTime,
+ @RequestParam @Nullable LocalDate endDate,
+ @RequestParam @Nullable LocalTime endTime) {
+ return super.getBetween(startDate, startTime, endDate, endTime);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java
new file mode 100644
index 000000000000..900db5e84d13
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java
@@ -0,0 +1,69 @@
+package ru.javawebinar.topjava.web.meal;
+
+import com.fasterxml.jackson.annotation.JsonView;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.Nullable;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.View;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealTo;
+import ru.javawebinar.topjava.util.ValidationUtil;
+
+import javax.validation.Valid;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/profile/meals", produces = MediaType.APPLICATION_JSON_VALUE)
+public class MealUIController extends AbstractMealController {
+
+ @Override
+ @GetMapping
+ @JsonView(View.JsonUI.class)
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @GetMapping( "/{id}")
+ @JsonView(View.JsonUI.class)
+ public Meal get(@PathVariable int id) {
+ return super.get(id);
+ }
+
+ @Override
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable int id) {
+ super.delete(id);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult result) {
+ if (result.hasErrors()) {
+ return ValidationUtil.getErrorResponse(result);
+ }
+ if (meal.isNew()) {
+ super.create(meal);
+ } else {
+ super.update(meal, meal.getId());
+ }
+ return ResponseEntity.ok().build();
+ }
+
+ @Override
+ @GetMapping("/filter")
+ @JsonView(View.JsonUI.class)
+ public List getBetween(
+ @RequestParam @Nullable LocalDate startDate,
+ @RequestParam @Nullable LocalTime startTime,
+ @RequestParam @Nullable LocalDate endDate,
+ @RequestParam @Nullable LocalTime endTime) {
+ return super.getBetween(startDate, startTime, endDate, endTime);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
new file mode 100644
index 000000000000..cfc5df83c205
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -0,0 +1,75 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.UsersUtil;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkIsNew;
+
+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 void create(UserTo userTo) {
+ log.info("create {}", userTo);
+ checkIsNew(userTo);
+ service.create(UsersUtil.createNewFromTo(userTo));
+ }
+
+ public User create(User user) {
+ log.info("create {}", user);
+ checkIsNew(user);
+ return service.create(user);
+ }
+
+ public void delete(int id) {
+ log.info("delete {}", id);
+ service.delete(id);
+ }
+
+ public void update(User user, int id) {
+ log.info("update {} with id={}", user, id);
+ assureIdConsistent(user, id);
+ service.update(user);
+ }
+
+ public void update(UserTo userTo, int id) {
+ log.info("update {} with id={}", userTo, id);
+ assureIdConsistent(userTo, id);
+ service.update(userTo);
+ }
+
+ public User getByMail(String email) {
+ log.info("getByEmail {}", email);
+ return service.getByEmail(email);
+ }
+
+ public User getWithMeals(int id) {
+ log.info("getWithMeals {}", id);
+ return service.getWithMeals(id);
+ }
+
+ public void enable(int id, boolean enabled) {
+ log.info(enabled ? "enable {}" : "disable {}", id);
+ service.enable(id, enabled);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
new file mode 100644
index 000000000000..dfc40e6c6b03
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
@@ -0,0 +1,71 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import ru.javawebinar.topjava.model.User;
+
+import java.net.URI;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = AdminRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class AdminRestController extends AbstractUserController {
+
+ static final String REST_URL = "/rest/admin/users";
+
+ @Override
+ @GetMapping
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @GetMapping("/{id}")
+ public User get(@PathVariable int id) {
+ return super.get(id);
+ }
+
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity createWithLocation(@RequestBody User user) {
+ User created = super.create(user);
+ URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
+ .path(REST_URL + "/{id}")
+ .buildAndExpand(created.getId()).toUri();
+ return ResponseEntity.created(uriOfNewResource).body(created);
+ }
+
+ @Override
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable int id) {
+ super.delete(id);
+ }
+
+ @Override
+ @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void update(@RequestBody User user, @PathVariable int id) {
+ super.update(user, id);
+ }
+
+ @Override
+ @GetMapping("/by-email")
+ public User getByMail(@RequestParam String email) {
+ return super.getByMail(email);
+ }
+
+ @GetMapping("/{id}/with-meals")
+ public User getWithMeals(@PathVariable int id) {
+ return super.getWithMeals(id);
+ }
+
+ @Override
+ @PatchMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void enable(@PathVariable int id, @RequestParam boolean enabled) {
+ super.enable(id, enabled);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java
new file mode 100644
index 000000000000..1da63b11b9b5
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java
@@ -0,0 +1,57 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+import ru.javawebinar.topjava.util.ValidationUtil;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/admin/users", produces = MediaType.APPLICATION_JSON_VALUE)
+public class AdminUIController extends AbstractUserController {
+
+ @Override
+ @GetMapping
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ @GetMapping("/{id}")
+ public User get(@PathVariable int id) {
+ return super.get(id);
+ }
+
+ @Override
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable int id) {
+ super.delete(id);
+ }
+
+ @PostMapping
+ public ResponseEntity createOrUpdate(@Valid UserTo userTo, BindingResult result) {
+ if (result.hasErrors()) {
+ return ValidationUtil.getErrorResponse(result);
+ }
+ if (userTo.isNew()) {
+ super.create(userTo);
+ } else {
+ super.update(userTo, userTo.id());
+ }
+ return ResponseEntity.ok().build();
+ }
+
+ @Override
+ @PostMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void enable(@PathVariable int id, @RequestParam boolean enabled) {
+ super.enable(id, enabled);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
new file mode 100644
index 000000000000..ccc43013a471
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,42 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.to.UserTo;
+
+import static ru.javawebinar.topjava.web.SecurityUtil.authUserId;
+
+@RestController
+@RequestMapping(value = ProfileRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
+public class ProfileRestController extends AbstractUserController {
+ static final String REST_URL = "/rest/profile";
+
+ @GetMapping
+ public User get() {
+ return super.get(authUserId());
+ }
+
+ @DeleteMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete() {
+ super.delete(authUserId());
+ }
+
+ @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void update(@RequestBody UserTo userTo) {
+ super.update(userTo, authUserId());
+ }
+
+ @GetMapping("/text")
+ public String testUTF() {
+ return "Русский текст";
+ }
+
+ @GetMapping("/with-meals")
+ public User getWithMeals() {
+ return super.getWithMeals(authUserId());
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml
new file mode 100644
index 000000000000..05589f71f06e
--- /dev/null
+++ b/src/main/resources/cache/ehcache.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+ 5
+
+ 5000
+
+
+
+
+
+
+ 1
+
+
+
+
diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties
new file mode 100644
index 000000000000..17c03ef4ebda
--- /dev/null
+++ b/src/main/resources/db/hsqldb.properties
@@ -0,0 +1,11 @@
+#database.url=jdbc:hsqldb:file:D:/temp/topjava
+
+database.url=jdbc:hsqldb:mem:topjava
+database.username=sa
+database.password=
+
+database.init=true
+jdbc.initLocation=classpath:db/initDB_hsql.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql
new file mode 100644
index 000000000000..4bf3d8446ac5
--- /dev/null
+++ b/src/main/resources/db/initDB.sql
@@ -0,0 +1,37 @@
+DROP TABLE IF EXISTS user_role;
+DROP TABLE IF EXISTS meal;
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS global_seq;
+
+CREATE SEQUENCE global_seq START WITH 100000;
+
+CREATE TABLE users
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ name VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ password VARCHAR NOT NULL,
+ registered TIMESTAMP DEFAULT now() NOT NULL,
+ enabled BOOL DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
+
+CREATE TABLE user_role
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR NOT NULL,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meal
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ user_id INTEGER NOT NULL,
+ date_time TIMESTAMP NOT NULL,
+ description TEXT NOT NULL,
+ calories INT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meal_unique_user_datetime_idx ON meal (user_id, date_time);
\ No newline at end of file
diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql
new file mode 100644
index 000000000000..9e0e195e600b
--- /dev/null
+++ b/src/main/resources/db/initDB_hsql.sql
@@ -0,0 +1,39 @@
+DROP TABLE user_role IF EXISTS;
+DROP TABLE meal IF EXISTS;
+DROP TABLE users IF EXISTS;
+DROP SEQUENCE global_seq IF EXISTS;
+
+CREATE SEQUENCE GLOBAL_SEQ AS INTEGER START WITH 100000;
+
+CREATE TABLE users
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ registered TIMESTAMP DEFAULT now() NOT NULL,
+ enabled BOOLEAN DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx
+ ON USERS (email);
+
+CREATE TABLE user_role
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR(255) NOT NULL,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meal
+(
+ id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
+ date_time TIMESTAMP NOT NULL,
+ description VARCHAR(255) NOT NULL,
+ calories INT NOT NULL,
+ user_id INTEGER NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meal_unique_user_datetime_idx
+ ON meal (user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql
new file mode 100644
index 000000000000..9e9bd828babe
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,25 @@
+DELETE FROM user_role;
+DELETE FROM meal;
+DELETE FROM users;
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+INSERT INTO users (name, email, password)
+VALUES ('User', 'user@yandex.ru', 'password'),
+ ('Admin', 'admin@gmail.com', 'admin'),
+ ('Guest', 'guest@gmail.com', 'guest');
+
+INSERT INTO user_role (role, user_id)
+VALUES ('USER', 100000),
+ ('ADMIN', 100001),
+ ('USER', 100001);
+
+INSERT INTO meal (date_time, description, calories, user_id)
+VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000),
+ ('2020-01-30 13:00:00', 'Обед', 1000, 100000),
+ ('2020-01-30 20:00:00', 'Ужин', 500, 100000),
+ ('2020-01-31 0:00:00', 'Еда на граничное значение', 100, 100000),
+ ('2020-01-31 10:00:00', 'Завтрак', 500, 100000),
+ ('2020-01-31 13:00:00', 'Обед', 1000, 100000),
+ ('2020-01-31 20:00:00', 'Ужин', 510, 100000),
+ ('2020-01-31 14:00:00', 'Админ ланч', 510, 100001),
+ ('2020-01-31 21:00:00', 'Админ ужин', 1500, 100001);
diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties
new file mode 100644
index 000000000000..c56854a9b452
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,10 @@
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=user
+database.password=password
+
+database.init=true
+jdbc.initLocation=classpath:db/initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+#https://hibernate.atlassian.net/browse/HHH-13280
+hibernate.use_sql_comments=false
\ No newline at end of file
diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties
new file mode 100644
index 000000000000..e11f0725f3bd
--- /dev/null
+++ b/src/main/resources/db/tomcat.properties
@@ -0,0 +1,5 @@
+database.init=false
+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..fe9e877a0d19
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+ ${TOPJAVA_ROOT}/log/topjava.log
+
+
+ UTF-8
+ %date %-5level %logger{50}.%M:%L - %msg%n
+
+
+
+
+
+ UTF-8
+ %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml
new file mode 100644
index 000000000000..3a75ebb82910
--- /dev/null
+++ b/src/main/resources/spring/spring-app.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-cache.xml b/src/main/resources/spring/spring-cache.xml
new file mode 100644
index 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..fec9d77513fb
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml
new file mode 100644
index 000000000000..6cdbe5fb8f5b
--- /dev/null
+++ b/src/main/resources/spring/spring-mvc.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/plain;charset=UTF-8
+ text/html;charset=UTF-8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml
new file mode 100644
index 000000000000..cd6c2ffee404
--- /dev/null
+++ b/src/main/resources/spring/spring-security.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/tomcat/context.xml b/src/main/resources/tomcat/context.xml
new file mode 100644
index 000000000000..e16eab586e23
--- /dev/null
+++ b/src/main/resources/tomcat/context.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+ WEB-INF/web.xml
+ ${catalina.base}/conf/web.xml
+
+
+
+
+
+
+
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp
new file mode 100644
index 000000000000..8e14ccf823dd
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp
@@ -0,0 +1,14 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp
new file mode 100644
index 000000000000..cf1331fd5523
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp
@@ -0,0 +1,8 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%--https://getbootstrap.com/docs/4.0/examples/sticky-footer/--%>
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp
new file mode 100644
index 000000000000..b69a412d72da
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp
@@ -0,0 +1,25 @@
+<%@page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%--http://stackoverflow.com/a/24070373/548473--%>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp
new file mode 100644
index 000000000000..9c24b9539073
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp
@@ -0,0 +1,14 @@
+<%@ page contentType="text/html" pageEncoding="UTF-8" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/jsp/login.jsp b/src/main/webapp/WEB-INF/jsp/login.jsp
new file mode 100644
index 000000000000..12838d661d29
--- /dev/null
+++ b/src/main/webapp/WEB-INF/jsp/login.jsp
@@ -0,0 +1,77 @@
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
+
+
+
+
+
+
+