diff --git a/.gitignore b/.gitignore
index b4860155a..e134710a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,8 @@ out
target
*.iml
log
-*.patch
\ No newline at end of file
+*.patch
+/.project
+/.classpath
+/.gitignore
+/.settings/
diff --git a/Procfile b/Procfile
new file mode 100644
index 000000000..4afeb7900
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: java $JAVA_OPTS -Dspring.profiles.active="datajpa,heroku" -DTOPJAVA_ROOT="." -jar target/dependency/webapp-runner.jar --port $PORT target/*.war
\ No newline at end of file
diff --git a/README.md b/README.md
index 118845c09..7fd61b43a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-[](https://app.codacy.com/gh/JavaWebinar/topjava/dashboard)
+[](https://www.codacy.com/gh/JavaWebinar/topjava/dashboard)
+
Java Enterprise Online Project
===============================
@@ -10,13 +11,14 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- [Wiki](https://github.com/JavaOPs/topjava/wiki)
- [Wiki Git](https://github.com/JavaOPs/topjava/wiki/Git)
- [Wiki IDEA](https://github.com/JavaOPs/topjava/wiki/IDEA)
-- [Демо разрабатываемого приложения](http://javaops-demo.ru/topjava)
+- [Демо разрабатываемого приложения](http://topjava.herokuapp.com/)
-### 25.09: Старт проекта
-- Начало проверки [вступительного задания HW0](https://github.com/JavaOPs/topjava#-Домашнее-задание-hw0)
+### 26.05: Старт проекта
+- Начало проверки [вступительного задания](https://github.com/JavaOPs/topjava#-Домашнее-задание-hw0)
-#### 30.09 Дедлайн на сдачу HW0
-### 02.10: 1-е занятие
+#### 31.05 Дедлайн на сдачу HW0
+### 02.06: 1-е занятие
+#### 03.06 Дедлайн подачи заявки на [дипломную программу](https://javaops.ru/view/register/diploma)
- Разбор домашнего задания вступительного занятия (вместе с Optional)
- Обзор используемых в проекте технологий. Интеграция ПО
- Maven
@@ -25,7 +27,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Уровни и зависимости логгирования. JMX
- Домашнее задание 1-го занятия (HW1 + Optional)
-### 09.10: 2-е занятие
+### 09.06: 2-е занятие
- Разбор домашнего задания HW1 + Optional
- Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava
- Слои приложения. Создание каркаса приложения
@@ -33,7 +35,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Пояснения к HW2. Обработка Autowired
- Домашнее задание (HW2 + Optional)
-### 16.10: 3-е занятие
+### 16.06: 3-е занятие
- Разбор домашнего задания HW2 + Optional
- Жизненный цикл Spring контекста
- Тестирование через JUnit
@@ -46,7 +48,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Логирование тестов
- Домашнее задание (HW3 + Optional)
-### 23.10: 4-е занятие
+### 23.06: 4-е занятие
- Разбор домашнего задания HW3 + Optional
- Методы улучшения качества кода
- Spring: инициализация и популирование DB
@@ -56,7 +58,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Домашнее задание (HW4 + Optional)
#### Начало выполнения [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md)
-### 30.10: 5-е занятие
+### 30.06: 5-е занятие
- Обзор JDK 9/17. Миграция Topjava с 1.8 на 17
- Разбор вопросов
- Разбор домашнего задания HW4 + Optional
@@ -67,7 +69,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Spring кэш
- Домашнее задание (HW5 + Optional)
-### 06.11: 6-е занятие
+### 07.07: 6-е занятие
- Разбор домашнего задания HW5 + Optional
- Кэш Hibernate
- Spring Web
@@ -80,7 +82,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
#### Большое ДЗ + выпускной проект + начинаем [курс BootJava](https://javaops.ru/view/bootjava) + подтягиваем "хвосты".
-### 20.11: 7-е занятие
+### 21.07: 7-е занятие
- Разбор домашнего задания HW6 + Optional
- Автогенерация DDL по модели
- Тестирование Spring MVC
@@ -91,7 +93,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Тестирование через SoapUi. UTF-8
- Домашнее задание (HW7 + Optional)
-### 27.11: 8-е занятие
+### 28.07: 8-е занятие
- Разбор домашнего задания HW7 + Optional
- WebJars. jQuery и JavaScript frameworks
- Bootstrap
@@ -100,7 +102,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Добавление Spring Security
- Домашнее задание (HW8 + Optional)
-### 04.12: 9-е занятие
+### 04.08: 9-е занятие
- Разбор домашнего задания HW8 + Optional
- Spring Binding
- Spring Validation
@@ -112,7 +114,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Cookie. Session
- Домашнее задание (HW9 + Optional)
-### 11.12: 10-е занятие
+### 11.08: 10-е занятие
- Разбор домашнего задания HW10 + Optional
- Кастомизация JSON (@JsonView) и валидации (groups)
- Рефакторинг: jQuery конверторы и группы валидации по умолчанию
@@ -125,22 +127,23 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery +
- Защита от межсайтовой подделки запросов (CSRF)
- Домашнее задание (HW10)
-### 18.12: 11-е занятие
+### 18.08: 11-е занятие
- Разбор домашнего задания HW10 + Optional
- Локализация datatables, ошибок валидации
- Защита от XSS (Cross Site Scripting)
- Обработка ошибок 404 (NotFound)
- Доступ к AuthorizedUser
- Ограничение модификации пользователей
-- Деплой приложения [на собственный выделенный сервер](https://github.com/JavaOPs/startup)
-- Домашнее задание (HW11): сокрытия полей в Swagger
-- Составление резюме. Собеседование. Разработка ПО. Возможные доработки приложения
-
-### 22.12: Миграция на Spring-Boot 3.5
-- Ревью вашего резюме
+- Деплой [приложения в Heroku](http://topjava.herokuapp.com)
+- Собеседование. Разработка ПО
+- Возможные доработки приложения
+- Домашнее задание по проекту: составление резюме
+
+### 22.08: Миграция на Spring-Boot
- Основы Spring Boot. Spring Boot maven plugin
- Lombok, база H2, ApplicationRunner
- Spring Data REST + HATEOAS
- Миграция приложения подсчета калорий на Spring Boot
-### 12.01: Дедлайн на сдачу [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md)
+### 11.09.22: Дедлайн на сдачу [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md)
+### 21.09.22: Получение дипломов для участников [Дипломной программы](https://javaops.ru/view/register/diploma)
diff --git a/config/db.properties b/config/db.properties
deleted file mode 100644
index cdec2d890..000000000
--- a/config/db.properties
+++ /dev/null
@@ -1,8 +0,0 @@
-database.url=jdbc:postgresql://localhost:5432/topjava
-database.username=user
-database.password=password
-database.init=false
-jdbc.initLocation=classpath:db/initDB.sql
-jpa.showSql=false
-hibernate.format_sql=false
-hibernate.use_sql_comments=false
\ No newline at end of file
diff --git a/config/messages/app.properties b/config/messages/app.properties
index 0afa010e2..9aeeda6aa 100644
--- a/config/messages/app.properties
+++ b/config/messages/app.properties
@@ -2,7 +2,7 @@ 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). \
+Meals could be filtered by date and time. Meal record color depends on daily calories sum exceeding "Daily calorie limit" (editable user's profile parameter). \
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
diff --git a/doc/lesson07.md b/doc/lesson07.md
new file mode 100644
index 000000000..0d3e09d53
--- /dev/null
+++ b/doc/lesson07.md
@@ -0,0 +1,694 @@
+# Стажировка Topjava
+
+## Материалы занятия
+
+###  Правки в проекте
+
+#### Apply 7_0_fix.patch
+- Мелкие правки.
+Если вы уже применяли патч в прошлом уроке - он немного изменился. Поправьте в `User` вместо `@JoinColumn(name = "id")` просто `@JoinColumn`
+Или можно сделать на патче `6_15_spring_i18n` [*Reset Current Branch to Here...->Hard*](https://github.com/JavaOPs/topjava/wiki/Git#user-content-revert) и применить новый `7_0_fix`
+
+##  Разбор домашнего задания HW6
+
+###  1. HW6
+
+#### Apply 7_01_HW6_fix_tests.patch
+
+
+ Краткое содержание
+
+#### Починить InMemory и JDBC тесты
+
+InMemory-тесты перестали работать, т.к ранее мы перенесли сканирование каталога `web` из `spring-app.xml` в конфигурацию `spring-mvc.xml`, которой нет в тестах. В результате контроллеры перестали
+попадать в спринг-контекст тестов. Для восстановления добавим сканирование каталога `web` в конфигурацию `inmemory.xml`. Теперь в классах, которые работают с InMemory-реализацией, для создания
+контекста можно оставить импорт только конфигурации
+`spring/inmemory.xml`.
+
+JDBC-тесты перестали работать, т.к в конфигурации `spring-db.xml` мы объявили бин `JpaUtil` только для профилей jpa и dataJpa, для других профилей (jdbc) этот бин создаваться не будет.
+JDBC-тесты мы запускаем с профилем jdbc, но в абстрактном классе AbstractUserServiceTest (общем для всех тестов сервисного слоя User) для всех профилей мы указали необходимость создания переменной
+типа `JpaUtil`. Соответственно, для профиля jdbc в контексте спринга будет отсутствовать этот бин, и спринг не сможет запустить приложение из-за неразрешенной зависимости.
+
+Чтобы спринг смог поднять контекст в профиле JDBC, нужно указать над переменной `jpaUtil`
+аннотацию `@Autowired(required = false)` - мы указываем спрингу, что эта зависимость не является обязательной и можно ее проигнорировать.
+
+> В новой версии заменил аннотацию на ленивую инициализацию `@Lazy`
+
+И в `@Before` методе тестов используем этот бин только для JPA реализаций.
+Для этого создадим утильный метод `isJpaBased()`, который будет проверять, относится ли текущая реализация к jpa. Чтобы проверить, с какими профилями запущен Spring, нам придется внедрить
+в `AbstractServiceTest`
+бин класса `Environment`. Это класс спринга, который позволит получить доступ к информации о том, с какими параметрами он был запущен, с помощью
+```env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.JPA, Profiles.DATAJPA))```
+С помощью этого же утильного метода теперь мы можем проверить, что для `MealServiceTest` тесты на валидацию `validateRootCause()` будут выполняться только для jpa/dataJpa профилей (если этот тест
+запустить для профиля jdbc, то он упадет, т.к. пока в JDBC у нас нет валидации).
+
+#### Локализация, jsp:include для meal*.jsp
+
+1. В файлы интернационализации `app.properties` добавляем дополнительные пары ключ-значение для русского и английского языка. В JSP страницах вместо текста, по аналогии со страницами для User,
+ указываем ключи, вместо которых спринг должен подставить локализованные сообщения.
+2. Для каждой JSP страницы для включения фрагментов указываем теги:
+
+`` - в нем определены title страницы, ссылка на статические ресурсы и базовая ссылка на корень приложения.
+
+`` - верхняя часть страниц, в ней определены ссылки для навигации по приложению.
+И в самом низу страниц:
+``
+
+Так как мы локализуем приложение с помощью Spring, на страницах нужно удалить тег:
+`` - с ним работает только jstl.
+
+3. Для того, чтобы на страницах получить доступ к корню приложения, используется
+ `"${pageRequest.request.contextPath}"` - эту ссылку на root удобнее вынести в `headTag` в виде [`` элемента](https://stackoverflow.com/a/40228804/548473), чтобы она вместе с этим
+ фрагментом добавлялась к каждой странице, и не требовалось бы ее везде дублировать.
+
+4. Чтобы видеть, к каким URL были привязаны контроллеры во время работы приложения, в `logback.xml` настроим уровень логирования для Spring web:
+ ``
+
+#### Перенести функциональность из `MealServlet` в контроллеры
+
+Чтобы не дублировать одну и ту же функциональность для REST- и JSP-контроллеров, создадим абстрактный
+`AbstractMealController` (от него будут наследоваться остальные Meal-контроллеры), куда перенесем все методы из
+`MealRestController`. JSP-контроллер будет работать с jsp-страницами. Каждый метод этого контроллера будет делегировать основную функциональность в родительский абстрактный контроллер.
+
+> **Внимание!**. Не делайте без нужды абстрактных контроллеров в своих выпускных проектах!
+
+Так как каждый метод этого контроллера должен отвечать за единственное действие, разнесем функциональность по разным методам, а доступ к самим методам разделим с помощью
+аннотации `@RequestMapping (@GetMapping / @PostMapping)`, в их параметрах укажем путь к endpoint, по которому можно обратиться к методу.
+
+При этом для всего контроллера также зададим `@RequestMapping("/meals")` (`value=` - параметр по умолчанию, можно не указывать). Это префикс запроса для всех методов контроллера.
+
+> Один из признаков "хорошего" контроллера, где не смешивается разная функциональность, - этот общий url. Для каждой функциональности в выпускных создавайте свой собственный контроллер!
+
+Для доступа к определенному методу контроллера нужно будет указать уникальный для нашего приложения "путь + http-метод", который складывается из маппинга к контроллеру, маппинга к нужному методу и
+http-метода, например:
+`GET {корень приложения} + "/meals" + "/delete"`
+`GET {корень приложения} + "/meals"`
+`POST {корень приложения} + "/meals"`
+Для `mealList.jsp` теперь не нужно с запросом дополнительно передавать тип действия, которое мы хотим совершить с едой, мы можем просто обратиться к нужному методу по его уникальному пути (endpoint,
+url).
+
+Если на этом шаге запустить приложение, то мы столкнемся с проблемой: при выполнении манипуляций и переходе по ссылкам путь портится.
+
+- путь к ресурсу по этой ссылке строится не от корня приложения (application context - topjava), а от текущего контекста сервлета (servlet context), например:
+ `localhost:8080/topjava/meals'+'/meals`
+ Также перестали работать стили, так как путь к статическим ресурсам тоже определяется неверно (посмотрите вкладку *Network* браузера).
+ Чтобы это исправить, добавим базовый URL в `headTag`:
+ `base href = "${pageContext.request.contextPath}/"`. **Теперь это станет url, от которой будут строиться все относительные ссылки на страницах**.
+
+Также некоторые методы контроллера в результате работы должны не просто вернуть название view, который Spring MVC должен отобразить, а совершить *redirect*. Для этого при возврате имени view
+дополнительно укажем ключевое слово `redirect:`, например, `redirect: /meals`.
+
+Последняя проблема — некорректное отображение текста в кодировке UTF-8. Spring предоставляет для ее решения стандартный фильтр, который будет перехватывать все запросы и ответы сервера и устанавливать
+им нужную кодировку: в `web.xml` подключим `encodingFilter`.
+
+
+
+> Инжекцию в `AbstractUserServiceTest.jpaUtil` сделал [`@Lazy`: не иннициализировать бин до первого использования](https://www.logicbig.com/tutorials/spring-framework/spring-core/lazy-at-injection-point.html).
+
+#### Apply 7_02_HW6_meals.patch
+
+> сделал фильтрацию еды через `get`: операция идемпотентная, можно делать в браузере обновление по F5
+
+### Внимание: чиним пути в следующем патче
+
+При переходе на AJAX `JspMealController` удалим за ненадобностью, возвращение всей еды `meals()` останется в `RootController`.
+
+#### Apply 7_03_HW6_fix_relative_url_utf8.patch
+
+-
+ Relative paths in JSP
+-
+ Spring redirect: prefix
+
+###  2. HW6 Optional
+
+
+ Краткое содержание
+
+#### Добавление еще одной роли для Admin
+
+1. В файле популирования базы данных `populateDB.sql` добавим для admin дополнительную роль `ROLE_USER`.
+
+2. В тестовых данных для него также добавим аналогичную роль.
+
+> После этого тесты, которые связаны с методом `getAll()`, перестали работать, потому что для получения
+> списка всех пользователей с их ролями в именованном запросе мы использовали **LEFT JOIN FETCH**.
+> Происходит объединение таблиц, в результирующей таблице вместо одной записи для админа появляются дублирующие записи для одного и того же пользователя.
+> - простой способ решения - исключить из запроса **LEFT JOIN FETCH**. Роли все равно будут загружены, так как они FetchType.EAGER.
+> - также можно добавить в запрос ключевое слово **DISTINCT(u)** - теперь в результирующей таблице будут содержаться только уникальные записи.
+
+#### Добавление транзакционности в JDBC реализацию репозитория
+
+Чтобы аннотация `@Transactional` стала работать во всех профилях Spring - в файле `spring-db.xml` вынесем из профиля jpa, dataJpa в общую конфигурацию для всех профилей тег:
+``````
+
+Для профиля jdbc настроим DatasourceTransactionManager, который будет управлять транзакциями:
+``
+
+
+
+``
+После этого в JDBC-репозитории мы можем расставить аннотации `@Transactional` аналогично jpa репозиториям, и действия станут выполняться транзакционно (
+напомню: `` для логирования информации по транзакциям)
+
+#### Чтобы JDBC репозиторий смог работать с множественными ролями пользователя:
+
+У пользователя добавим сеттер для его ролей. Для JDBC-репозитория создадим вспомогательные методы для записи ролей в базу и их считывания из базы и установления пользователю. Запись ролей в базу будем
+производить методом
+`JdbcTemplate#batchUpdate`, в таком случае не будет обращения в базу для записи каждой конкретной роли, команды для записи ролей будут накоплены в один пакет и выполнятся за одно обращение к БД. Для
+удобства работы с batch Spring предоставляет нам интерфейс `BatchPreparedStatementSetter`, с помощью которого мы определяем как будут устанавливаться параметры для запроса и количество запросов в
+одном пакете. Также создадим метод `deleteRoles`, в котором будем удалять роли пользователя из базы (для обновления ролей в базе мы делаем просто: сначала удалим старые из базы и запишем туда новые).
+
+> PS: в JPA с `@ElementCollection` и с параметром *cascade* в `@OneToMany` слияние (merge) изменений в связанных коллекциях происходит автоматически.
+
+Если мы будем получать всех пользователей вместе с их ролями из базы с помощью JOIN, мы столкнемся с проблемой Декартова произведения: для каждого уникального пользователя количество записей в
+результирующей таблице будет повторяться столько раз, сколько у него было ролей. Чтобы этого избежать, отдельным запросом получим из базы все роли, и сгруппируем их в `Map` по `userId`, где ключом
+будет являться `userId`, а значением — набор ролей пользователя. После чего пройдемся по всем пользователям, загруженным из базы, и установим каждому его роли.
+
+
+#### Apply 7_04_HW6_optional_add_role.patch
+
+> - Для доставания ролей у нас дублируется `fetch = EAGER` и `LEFT JOIN FETCH u.roles` (можно делать что-то одно). Запросы выполняются по-разному: проверьте.
+
+- Отключил `JdbcUserServiceTest` - роли не работают. Будем чинить в `7_06_HW6_jdbc_transaction_roles.patch`
+- `DataJpaUserServiceTest.getWithMeals` не работает для admin (у админа 2 роли, и еда при JOIN дублируется). Чиним в следующем патче.
+
+#### Apply 7_05_fix_hint_graph.patch
+
+- В `DataJpaUserServiceTest.getWithMeals()` при дублировании еды `DISTINCT` при нескольких JOIN не помогает, оставил в `@EntityGraph` только `meals`.
+- В `JpaUserRepositoryImpl.getByEmail` и `CrudUserRepository.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.
+
+Еще один вариант решения - в `User` сделать `Set`. Интересно, что в ее реализации `PersistentSet`порядок соблюдается и `@OrderBy` работает.
+
+#### Apply 7_06_HW6_jdbc_transaction_roles.patch
+
+> - в `JdbcUserRepositoryImpl.getAll()` собираю роли из `ResultSet` напрямую в `map`
+> - в `insertRoles` поменял метод `batchUpdate` и сделал проверку на empty
+> - в `setRoles` достаю роли через `queryForList`
+
+Еще интересные JDBC реализации:
+
+- в `getAll()/ get()/ getByEmail()` делать запросы с `LEFT JOIN` и сделать реализацию `ResultSetExtractor`
+- подключить зависимость `spring-data-jdbc-core`. Там есть готовый `OneToManyResultSetExtractor`. Можно посмотреть, как он реализован.
+- реализация, зависимая от БД: доставать агрегированные роли и делать им `split(",")`. В этой реализации у нас ограничение - одно поле из зависимой таблицы.
+
+```
+SELECT u.*, string_agg(r.role, ',') AS roles
+FROM users u
+ JOIN user_roles r ON u.id=r.user_id
+GROUP BY u.id
+```
+
+### Валидация для `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` и проверкой профилей мы можем полностью отключить Spring-кэш в тестах через пустую реализацию `NoOpCacheManager`.
+Кэш Hibernate второго уровня отключаем через переопределение свойства `entityManagerFactory.jpaPropertyMap: hibernate.cache.use_second_level_cache=false` (кроме стандартного использования файла
+пропертей, можно задать их прямо в конфигурации, через автодополнение в xml можно смотреть все варианты). Подкладываем новый `spring-cache.xml` в ресурсы тестов, он перекроет настройки кэша в
+приложении. Остается удалить наши уже ненужные `JpaUtil` и `AbstractServiceTest.isJpaBased()`
+
+#### 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`
+> - поменял реализацию `ActiveDbProfileResolver`: в профили аттрибута `@ActiveProfiles(profiles=..)` он добавляет `Profiles.getActiveDbProfile()`
+> - сделал вспомогательный метод `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-platform-launcher` в pom для запуска JUnit 5 тестов из IDEA](https://youtrack.jetbrains.com/issue/IDEA-231927)
+
+- [JUnit 5 homepage](https://junit.org/junit5)
+- [Overview](https://junit.org/junit5/docs/snapshot/user-guide/#overview)
+- [Миграция с JUnit4 на JUnit5: важные отличия и преимущества](https://topjava.ru/blog/migratsiya-s-junit4-na-junit5)
+- [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
+> - Переделал URL поиска по email на `/by-email`
+
+- Понимание 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).
+Теперь спринг будет автоматически использовать эту библиотеку для сериализации/десериализации объектов в 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` - утильный класс и не является бином спринга, а для его работы требуется наш кастомный маппер, который находится под управлением спринга и расположен в
+контейнере зависимостей. Поэтому, чтобы была возможность получить наш маппер из других классов - сделаем его синглтоном и сделаем в нем статический метод, который будет возвращать его экземпляр.
+Теперь `JsonUtil` сможет его получить.
+И нам нужно указать спрингу, чтобы он не создавал второй экземпляр этого объекта, а клал в свой контекст существующий. Для этого в конфигурации `spring-mvc.xml` определим factory-метод, с помощью
+которого спринг должен получить экземпляр (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
+> - Сделал внутренний класс `MatcherFactory.Matcher`, который возвращается из фабрики матчеров.
+> - Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил.
+> - Раньше в тестах я для проверок получал всех users из базы и сравнивал с эталонным списком. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении-удалении новых тестовых данных.
+
+- [Java @SafeVarargs Annotation](https://www.baeldung.com/java-safevarargs)
+
+###  9. [Тестирование через SoapUi. UTF-8](https://drive.google.com/open?id=0B9Ye2auQ_NsFVXNmOUdBbUxxWVU)
+
+
+ Краткое содержание
+
+SOAP UI - это один из инструментов для тестирования API приложений, которые работают по REST и по SOAP.
+Он позволяет нам по HTTP протоколу дернуть методы нашего API и увидеть ответ контроллеров.
+
+Если в контроллер мы добавим метод, который в теле ответа будет возвращать текст на кириллице, то мы увидим кодировка теряться. Для сохранения кодировки используем `StringHttpMessageConverter`,
+который конфигурируем в `spring-mvc.xml`. При этом мы должны явно указать, что конвертор будет работать только с текстом в кодировке *UTF-8*.
+
+
+
+#### Apply 7_16_soapui_utf8_converter.patch
+
+- Инструменты тестирования REST:
+ - SoapUi
+ - [Что такое Curl? Как работает эта команда?](https://highload.today/curl/)
+ - Написание 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)
+
+##  Ваши вопросы
+
+> Зачем у нас и UIController'ы, и RestController'ы? То есть в общем случае backend-разработчику недостаточно предоставить REST-api и RestController?
+
+Часто используются и те и другие. REST обычно используют для отдельного UI например на React или Angular или для интеграции / мобильного приложения.
+У нас REST контроллеры используются только для тестирования. UI мы используем для нашего приложения на JSP шаблонах.
+Таких сайтов без богатой UI логики тоже немало. Например https://javaops.ru/ :)
+Разница в обработке запросов:
+
+- из UI контроллеров возвращаются как готовые HTML странички, так и данные в формате JSON (будет для AJAX запросов в следующих занятиях)
+- для UI мы используем только GET и POST запросы
+- при создании-обновлении в UI мы принимаем данные из формы `application/x-www-form-urlencoded` (посмотрите вкладку `Network`, не в формате JSON)
+- для REST запросы GET, POST, PUT, DELETE, PATCH и возвращают только данные (обычно JSON)
+
+И в способе авторизации:
+
+- для RESТ у нас будет базовая авторизация
+- для 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.*`. JUnit 4 включает в себя `hamcrest-core`, в JUnit 5 его нужно подключать отдельно.
+
+> 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: При проблемах с собственным форматтером убедитесь, что в конфигурации `Topjava
+
+## Материалы занятия
+
+- **Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере (в хроме `Ctrl+F5`)**
+- **При удалении файлов не забывайте делать clean: `mvn clean`**
+
+###  Правки в проекте
+
+#### Apply 8_0_fix.patch
+Время еды приходит с UI с точностью до минут
+
+##  Разбор домашнего задания HW7
+
+###  1. [HW7](https://drive.google.com/file/d/1h6wg2V9yZoNX7fA7mNA7w7Kxp8IACsIJ)
+
+
+ Краткое содержание
+
+#### Тесты ResourceController
+Прежде всего в настройках логирования для класса `ExceptionHandlerExceptionResolver`
+установим уровень "debug". Теперь в логах мы сможем увидеть запросы, у которых проблемы с маппингом.
+Чтобы протестировать доступ к ресурсам, создадим `ResourceControllerTest` с единственным тестовым методом.
+Класс `MediaType` позволяет указать требуемый тип с помощью фабричного метода `valueOf`.
+Начиная с [Spring 4.3 ожидаемый тип ответа нужно сравнивать с помощью `contentTypeCompatibleWith`](https://github.com/spring-projects/spring-framework/issues/19041), а не `contentType`
+(в этом случае кодировка UTF-8 в типе ответа не учитывается в сравнении).
+
+#### Тесты для RootController на еду
+Для `RootController` тесты на еду делаем точно так же, как и на `User`, с небольшим отличием.
+Так как `MealTo` - это транспортный объект, который не является Entity и не находится под управлением
+JPA, у него нет ограничений по методам `equals / hashCode`, и мы можем
+добавить свои (сгенерировать с помощью IDEA). Теперь в тестах объекты `MealTo` мы можем сравнивать
+через `equals()`.
+Чтобы убедиться что два списка `MealTo` - ожидаемый, и полученный от контроллера, сравниваются поэлементно
+через `equals`, мы можем установить в сравнении брекпоинт и запустить тест в режиме дебага.
+
+#### Реализовать MealRestController
+`MealRestController` реализуем аналогично контроллерам пользователей.
+В метод `MealRestController#getBetween` с параметрами запроса нужно передать
+время и дату начала и конца диапазона, для которого будет найдена еда. Это можно сделать с помощью аннотации `@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)`.
+Spring автоматически конвертирует параметры запроса в объекты типа `LocalDateTime`.
+
+В `MealRestControllerTest` нужно обратить внимание на тесты
+для методов `get` и `getBetween` контроллера, так как они возвращают список `MealTo`, а не `Meal`.
+Поэтому для сравнения списков еды создадим отдельный `TO_MATCHER` с помощью статического фабричного метода `usingEqualsComparator(MealTo.class)`:
+```
+public static MatcherFactory.Matcher TO_MATCHER = MatcherFactory.usingEqualsComparator(MealTo.class)
+```
+Он будет сравнивать `MealTo` уже не рекурсивно, а с помощью `MealTo#equals()` — сравнения в методах `assertMatch` переделал с использованием реализаций интерфейса `BiConsumer`:
+*assertion* и *iterableAssertion*. Получается очень гибко (привет, паттерн "стратегия"): для создания матчера мы можем использовать любые собственные реализации сравнений.
+
+Для того чтобы для тестов создать объекты `MealTo`, используем утилитный метод `MealsUtil#createTo`, изменив у него модификатор доступа на *public*.
+
+Для некоторых методов с переменным количеством аргументов IDEA сообщает о небезопасности типов. Чтобы подавить эти
+предупреждения, над методами у нас стоят аннотации `@SafeVarargs` (для использования этой аннотации метод должен быть `final`).
+
+Чтобы Jackson мог сериализовать/десериализовать объекты `MealTo`, нам нужно сделать для этого класса сеттеры, или создать конструктор, помеченный специальной аннотацией `@ConstructorProperties`,
+в параметры которой передаем поля объекта json, соответствующие аргументам конструктора.
+
+
+
+
+
+#### Apply 8_01_HW07_controller_test.patch
+
+- [Persistent classes implementing equals and hashcode](https://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html_single/#persistent-classes-equalshashcode): переопределять `equals()/hashCode()`
+ необходимо, если
+ - использовать Entity в `Set` (рекомендовано для many-ассоциаций) либо как ключи в `HashMap`
+ - использовать _reattachment of detached instances_ (т.е. манипулировать одним Entity в нескольких транзакциях/сессиях).
+- Оптимально использовать уникальные неизменяемые бизнес-поля, но обычно таких нет, и чаще всего используется суррогатный PK с ограничением, что он может быть `null` у новых объектов и нельзя объекты сравнивать
+ через `equals` в бизнес-логике (например, в тестах).
+- [Equals() and hashcode() when using JPA and Hibernate](https://stackoverflow.com/questions/1638723)
+
+------------------------
+
+#### Apply 8_02_HW07_rest_controller.patch
+> - В `MealTo` вместо изменяемых полей и конструктора без параметров сделал [`@ConstructorProperties`](https://www.logicbig.com/tutorials/misc/jackson/constructor-properties.html). `Immutable` классы
+ всегда предпочтительнее для данных.
+- [Паттерн стратегия](https://refactoring.guru/ru/design-patterns/strategy).
+
+###  2. HW7 Optional
+
+
+ Краткое содержание
+
+#### Собственный Spring-конвертер (форматтер) для даты и времени
+Spring фраймворк с помощью встроенных конвертеров (реализующих интерфейс `org.springframework.core.convert.converter.Converter`) и форматтеров (интерфейс `org.springframework.format.Formatter`) делает автоматическое преобразование параметров запроса из одного типа в другой.
+В нашем случае параметры фильтрации еды - дата и время - по REST приходят в виде строки, и мы можем добавить свой конвертер или форматтер, чтобы он автоматически приводил их к нужному нам типу.
+> - Конвертер Spring преобразует объект одного типа в объект другого типа
+> - Форматер преобразует объект типа String в объект нужного типа (при этом может поддерживать локаль)
+
+Сделаем собственные форматтеры для преобразования строки в дату и время `DateTimeFormatters`, добавим в `spring-mvc.xml` бин `conversionService` с перечнем наших форматтеров и сделаем на него ссылку:
+```
+
+```
+`LocalTimeFormatter` и `LocalDateFormatter` - наши кастомные форматтеры, которые будут парсить строку параметра. Для этого они должны реализовывать
+интерфейс `Formatter<Целевой тип>` и переопределять его методы `#parse` и `#print`. Теперь мы можем убрать аннотации `@DateTimeFormat` из аргументов `MealRestController#getBetween`. `conversionService` будет
+искать среди форматеров или конвертеров те, которые смогли бы преобразовать параметр-строку в объект соответствующего типа, объявленный в методе контроллера, и в результате будут использованы наши кастомные форматеры.
+Для новой реализации метода `getBetween` теперь создадим несколько тестов - с различным набором параметров (в том числе и с пустыми параметрами).
+
+#### Протестировать сервисы с помощью SoapUI
+Помимо SoapUI, для тестирования REST можно использовать команду *curl* через *Git Bash* (этот способ имеет свои недостатки - не поддерживается UTF8).
+Для запросов требуется указывать Content-Type, иначе контроллер не сможет обработать запрос.
+Также популярными средствами тестирования REST являются *Postman* и в IDEA: *Tools->HTTP Client*.
+> Для тестирования REST у вас должен быть запущен Tomcat с вашим приложением!
+
+
+
+
+#### Apply 8_03_HW07_formatters.patch
+
+> - Перенес форматтеры в подпакет `web`, т.к. они используются Spring MVC
+> - Заменил `@RequestParam(required = false)` на `@RequestParam @Nullable`
+
+#### Apply 8_04_HW07_soapui_curl.patch
+
+> Добавил примеры запросов curl в `config/curl.md`
+
+- Написание HTTP-запросов с помощью Curl (для Windows можно использовать Git Bash)
+- В IDEA появился отличный инструмент тестирования запросов. Для конвертации
+ в [Tools->HTTP Client->Test RESTful Web Service](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) скопируйте curl без флага `-s`
+
+### Внимание! curl команды, требуемые в ТЗ к выпускному проекту, сделайте в `readme.md`, НЕ НАДО делать в выпускном проекте отдельный `curl.md`.
+
+###  3. [HW7 Optional: getWithMeals + тесты](https://drive.google.com/file/d/13cjenXzWDr52HTTzleomOd-yjPAEAbOA)
+
+
+ Краткое содержание
+
+В нашем приложении у `Meal` есть ссылка на `User`, а в `User` есть ссылка на коллекцию `Meal`.
+Таким образом, мы имеем дело с *BiDirectional* циклической зависимостью. При сериализации через Jackson у нас возникнут проблемы, так как он перейдет в
+бесконечный цикл при переходе по ссылкам сущностей друг на друга.
+Возможно следующее разрешение циклических зависимостей:
+
+- над полем `Meal.user` добавить аннотацию `@JsonBackReference`, теперь для еды это поле не будет сериализоваться в json;
+- над коллекцией `User.meals` добавить аннотацию `@JsonManagedReference`, поле будет сериализоваться.
+
+Теперь для получения пользователя с едой в методах контроллера можно просто вызвать соответствующий метод сервиса.
+
+Для новой функциональности создадим дополнительные тесты. В тестовых данных для пользователей заполним поля *meals*.
+Чтобы сразу проверять пользователя вместе с его едой, создадим дополнительный `UserTestData.WITH_MEALS_MATCHER`, который будет сравнивать сущности с помощью переданных ему интерфейсов сравнения.
+Коллекции пользователей с едой мы не реализуем, поэтому `iterableAssertion` также делать не нужно, бросаем `UnsupportedOperationException`.
+
+Так как метод получения пользователя с едой у нас реализован только в профиле datajpa, в тестах перед выполнением метода нужно проверить, что текущий профиль Spring - `dataJpa`, тесты будут пропускаться для других профилей.
+Такую функциональность мы ранее уже реализовывали - внедряем в тестовый класс `Environment` и проверяем активный профиль с помощью `Assumptions#assumeTrue`.
+
+
+
+#### Apply 8_05_HW07_with_meals.patch
+#### Apply 8_06_HW07_test_with_meals.patch
+> Изменения в AssertJ: `ignoringAllOverriddenEquals` для рекурсивных сравнений не нужен. См. [overridden equals used before 3.17.0](https://assertj.github.io/doc/#assertj-core-recursive-comparison-ignoring-equals)
+
+## Занятие 8:
+
+###  4. WebJars. jQuery and JavaScript frameworks
+
+
+ Краткое содержание
+
+**WebJars** — библиотеки на стороне клиента (JavaScript библиотека и/или CSS модуль), упакованные в JAR.
+
+Добавим в наш проект в `pom.xml` дополнительные зависимости - библиотеки JavaScript и css:
+- *jQuery* - самая распространенная утилитная JavaScript-библиотека;
+- *Bootstrap* - фреймворк CSS-стилей;
+- *Datatables* - плагин для отрисовки таблиц;
+- *datetimepicker* - плагин для работы с датой и временем;
+- *noty* - для работы с уведомлениями;
+
+
+
+#### Apply 8_07_webjars.patch
+
+> - Обновил jQuery до 3.x, Bootstrap до 4.x
+ > - Новое в jQuery 3
+> - УБРАЛ из проекта Dandelion обертку к Datatables
+ > - не встречал нигде, кроме Spring Pet Clinic;
+ > - поддержка работы с Datatables через Dandelion оказалось гораздо более трудоемкой, чем работа с плагином напрямую.
+> - Исключил ненужные зависимости
+
+- Подключение веб-ресурсов. WebJars.
+- Introducing WebJars
+- Document Object Model (DOM)
+- What is the DOM?
+- jQuery
+- Is jQuery a javascript library or framework
+- DataTables
+- Working with jQuery DataTables
+
+##  5. [Bootstrap](https://drive.google.com/file/d/1RHtzw8OQt6guCu6xe3apT7F9EfiX96tr)
+
+
+ Краткое содержание
+
+Front-end нашего приложения будет строиться на основе фреймворка Bootstrap.
+> В новой версии Bootstrap 5 из зависимостей исключена библиотека jQuery, и весь необходимый функционал Bootstrap делается на простом JavaScript. Однако JQuery нам нужна для *Datatables* и плагинов, поэтому не стал переходить на 5-ю версию.
+
+По ссылке [Bootstrap Examples](https://getbootstrap.com/docs/4.6/examples/) приведены примеры сайтов на Bootstrap. Из перечня уже готовых шаблонов можно выбрать
+подходящий шаблон, скопировать из его исходного кода стили, форматирование и использовать в своем проекте.
+- В `spring-mvc.xml` мы должны явно указать маппинг на *WebJars*-ресурсы, с которыми будет работать приложение:
+````xml
+
+````
+- В `headTag.jsp`, который у нас сейчас добавляется через `jsp:include` в начало каждой JSP страницы, подтягиваем из *WebJars* нужные нам *css*-ресурсы и иконку для нашего приложения.
+- Для отрисовывания стандартных иконок подключается ресурс ``.
+ В класс иконок `.fa` добавим `cursor: pointer` - это курсор-рука, который обычно используется для кнопок.
+- В стили добавим sticky-footer - это footer, который будет включаться в конце JSP-страниц и приклеиваться к нижней части экрана.
+- JSP-страницу со списком пользователей оформим с использованием элементов Bootstrap и добавим иконки на кнопки.
+- на странице `index.jsp` форму выбора пользователя поместим в класс Bootstrap *jumbotron* - крупный выносной элемент с большим текстом и большими отступами
+- таблицей пользователей в `users.jsp` поместим в аналогичный элемент *jumbotron*
+
+
+#### Apply 8_08_bootstrap4.patch
+
+> - [WIKI Bootstrap](https://ru.wikipedia.org/wiki/Bootstrap_(фреймворк))
+> - Добавил Font Awesome
+ > - [Map glyphicon icons to font-awesome](https://gist.github.com/blowsie/15f8fe303383e361958bd53ecb7294f9)
+> - В `headTag.jsp` в ссылку на `style.css` добавил `?v=2`. Стили изменились, изменяя версию в параметре мы заставляем браузер не брать их из кэша, а загружать заново.
+
+- [Bootstrap](https://getbootstrap.com/)
+ - [Navbar](https://getbootstrap.com/docs/4.1/components/navbar/)
+ - [Spacing](https://getbootstrap.com/docs/4.1/utilities/spacing/)
+ - [Forms](https://getbootstrap.com/docs/4.1/components/forms/)
+ - [Sticky footer](https://getbootstrap.com/docs/4.1/examples/sticky-footer/)
+- [Документация Bootstrap на русском](https://bootstrap-4.ru/)
+- Дополнительно
+ - Twitter Bootstrap Tutorial
+ - Видеоуроки Bootstrap 4
+ - [Bootstrap верстка современного сайта за 45 минут](https://www.youtube.com/watch?v=46q2eB7xvXA)
+
+##  Ваши вопросы
+
+> А где реально этот путь "classpath:/META-INF/resources/webjars"?
+
+Внутри подключаемых webjars ресурсы лежат по пути `/META-INF/resources/webjars/...` Не поленитесь посмотреть на них через `Ctrl+Shift+N`. Все подключаемые jar попадают в classpath, и ресурсы доступны
+по этому пути.
+
+> У меня webjars-зависимость лежит внутри ".m2\repository\org\webjars\". С чем это может быть связано?
+
+Maven скачивает все зависимости в local repository, который по умолчанию находится в `~/.m2`. Каталог по умолчанию можно переопределить в `APACHE-MAVEN-HOME\conf\settings.xml`,
+элемент `localRepository`.
+
+> WEBJARS лежат вообще в другом месте WEB-INF\lib\. Биндим mapping="/webjars/*" на реальное положение jar в war-e, откуда Spring знает, где искать наш jQuery?
+
+В war в `WEB-INF/lib/*` лежат все jar, которые попадают к classpath. Spring при обращении по url `/webjars/` ищет по пути
+биндинга ``
+по всему classpath (то же самое, как распаковать все jar в один каталог) в `META-INF/resources/webjars/`. В этом месте во всех jar, которые мы подключили из webjars, лежат наши ресурсы.
+
+> Оптимально ли делать доступ к статическим ресурсам (css, js, html) через webjars ?
+
+На продакшене под нагрузкой статические ресурсы лучше всего держать не в war, а снаружи. Доступ к ним делается либо
+через конфигурирование Tomcat.
+Но чаще всего для доступа к статике ставят прокси, например Nginx
+
+##  6. AJAX. Datatables. jQuery
+
+
+ Краткое содержание
+
+**AJAX** (асинхронный JavaScript и XML) — подход к построению интерактивных пользовательских интерфейсов веб-приложений, заключающийся в "фоновом" обмене данными браузера с веб-сервером.
+
+#### AdminUIController
+У нас будут отдельные от REST UI-контроллеры, так как в них будут отличаться обработка исключений, некоторая логика и авторизация.
+В `AdminUIController` метод `#create` будет использоваться как для создания, так и для обновления пользователя в зависимости от значения `id`.
+
+#### Список пользователей
+Оформляем таблицу пользователей с помощью js/css библиотеки `Datatables`. Таблица должна иметь id (в нашем случае "datatable"), чтобы к ней можно было обращаться.
+Также на страницу добавляем форму, с помощью которой будем редактировать и добавлять пользователей.
+Форма имеет скрытое поле `id`, которое будет использоваться в наших js-скриптах.
+
+#### topjava.users.js
+> Код по сравнению с видео изменился! Про изменения я говорю в конце видео и перечислил их после *Краткого содержания*
+
+Для работы AJAX объявляем переменные:
+- *ajaxUrl* - адрес нужного endpoint контроллера
+- *datatableApi* - объект таблицы `datatable`
+
+Страница html имеет определенный жизненный цикл, в процессе которого с ней совершаются какие-то действия.
+Одно из таких действий - загрузка, после которого мы можем производить какие-то манипуляции на странице.
+С помощью jQuery мы определяем коллбэк-метод, который будет вызываться после загрузки страницы:
+```
+$(function () {
+ ...
+```
+Строчка
+```
+datatableApi = $("#datatable").DataTable(
+```
+преобразует HTML-элемент c *id=datatable* в javaScript-объект с помощью метода `DataTable` библиотеки *Datatables*.
+Параметр этого метода - объект-конфигурация, который задает опции отображения таблицы и в "columns" задает соответствие колонок таблицы полям приходящего с сервера JSON-объекта пользователей.
+Внизу конфигурации добавляется сортировка таблицы по первому столбцу.
+После этого вызывается метод `makeEditable()` (он находится в `topjava.common.js`).
+
+#### topjava.common.js
+
+- В `makeEditable` к событию *click* всех объектов HTML c классом *delete* привязываем вызов метода `deleteRow`. Параметром берем аттрибут `id` текущего элемента `$(this)`.
+
+- Метод `add` вызывается из `users.jsp` по нажатию на кнопку "Добавить": `onclick="add()"`. В нем
+ - обнуляются все поля `input` формы `detailsForm`: `$("#detailsForm").find(":input").val("")`
+ - вызывается входящий в Bootstrap метод `modal()`, который преобразует HTML-элемент `id=editRow` в модальное окно. [Botstrap4 Modal](https://getbootstrap.com/docs/4.6/components/modal)
+
+- В методе `deleteRow` делаем AJAX-запросы к серверу и по после их успешного выполнения вызываем обновление таблицы.
+
+- В `updateTable` по AJAX запрашиваем с сервера массив пользователей, в случае успеха очищаем таблицу и заполняем ее данными, полученными с сервера.
+
+- В `save` средствами jQuery сериализуем форму `id=detailsForm` в JSON-объект и методом POST отдаем эти данные. После успешного выполнения запроса закрываем модальное окно и обновляем таблицу.
+
+Intellij IDEA предоставляет нам возможность дебага кода JavaScript. См. видео для примера.
+
+#### Загрузка HTML
+По умолчанию при стандартной загрузке страницы с js-скриптами браузер будет:
+- Парсить нужную HTML-страницу;
+- Как только браузер сталкивается с тегом `` и сохранить.**
+
+- XSS для новичков
+- XSS глазами злоумышленника
+
+Раньше я [реализовывал XSS защиту через `@SafeHtml`](https://stackoverflow.com/a/40644276/548473), пока его не [удалили из hibernate validator](https://hibernate.org/validator/documentation/migration-guide/).
+Пришлось сделать собственную аннотацию `@NoHtml` на основе [Sanitizing User Input](https://thoughtfulsoftware.wordpress.com/2013/05/26/sanitizing-user-input-part-ii-validation-with-spring-rest/)
+ и [jsoup - Sanitize HTML](https://www.tutorialspoint.com/jsoup/jsoup_sanitize_html.htm)
+Все классы, относящиеся к валидации перенес в пакет `ru.javawebinar.topjava.util.validation`
+- `password` проверять не надо, т.к. он не выводится в html, а [email надо](https://stackoverflow.com/questions/17480809)
+- Сделать общий интерфейс валидации `View.Web` и `@Validated(View.Web.class)` вместо `@Valid` для проверки содержимого только на входе UI/REST.
+При сохранении в базу проверка на безопасный html контент (XSS) повторно не делается.
+- [Validation groups in Spring MVC](https://blog.codeleak.pl/2014/08/validation-groups-in-spring-mvc.html)
+
+#### Apply 11_12_XSS.patch
+
+### Swagger2
+
+Swagger это фреймворк для автоматического создания REST-API документации по аннотациям контроллеров Spring MVC.
+Подключим зависимость `springfox-swagger2` и `springfox-swagger-ui` в `pom.xml`.
+Сразу же в проект подключается Swagger UI интерфейс, который позволяет отправлять запросы к эндпоинтам REST-API и просматривать документацию.
+
+Настройка swagger производится в конфигурации `spring-mvc.xml` подключением бина `Swagger2DocumentationConfiguration`.
+Чтобы смотреть REST-API документацию мог любой пользователь, в `spring-security.xml` доступ к эндпоинтам Swagger UI открываем всем:
+```xml
+
+
+
+```
+`AuthorizedUser authUser` не является реальным параметром методов контроллера, который передается клиентом.
+Это авторизированный пользователь, который резолвится Spring Security через `@AuthenticationPrincipal`.
+Убираем его из документации запросов через `@ApiIgnore`: Swagger будет игнорировать такие параметры при генерировании документации.
+UI контроллеры также исключаем из REST-API, пометив их `@ApiIgnore` на уровне класса.
+
+Создадим на `login.jsp` кнопку "Swagger REST Api Documentation".
+
+**Внимание: Swagger подключается в проект ОЧЕНЬ просто, а пользу от него для ревью трудно переоценить. Вместо примеров `curl` в выпускных проектах
+предлагаю вам подключить Swagger и в `readme.md` дать ссылку на сгенерированную REST API документацию.**
+
+- [Setting Up Swagger 2 with a Spring REST API](https://www.baeldung.com/swagger-2-documentation-for-spring-rest-api)
+- [Swagger 2 Configuration With Spring (XML)](https://medium.com/@andreymamontov/swagger-2-configuration-with-spring-xml-3cd643a12425)
+- [Hiding Endpoints From Swagger Documentation](https://www.baeldung.com/spring-swagger-hiding-endpoints)
+> В версиях выше 2.10 и 3.0 появились проблемы с маппингом. Вариант документации c OpenAPI 3.0 смотрите в [Spring Boot курсе](https://javaops.ru/view/bootjava)
+
+#### Apply 11_13_swagger2.patch
+
+---------------------
+### ДЗ Optional 2
+
+Обратите внимание в Swagger UI на `Example Value` при
+- `POST /rest/admin/users (createWithLocation)` - здесь не должно быть поля `meals`. Пользователь создается без еды, еда управляется своими запросами.
+- `POST /rest/profile/meals (createWithLocation)` - здесь не должно быть поля `user`.
+Нужно поправит `Example Value`. При этом учтите, что в API у нас есть метод
+`GET /rest/admin/users/{id}/with-meals (getWithMeals)` - вернуть пользователя с едой.
+
+[Hide a Request Field in Swagger API](https://www.baeldung.com/spring-swagger-hide-field)
+
+-----------------------
+
+### Ограничение модификации пользователей
+Наше демо-приложение доступно любому и нам нужно защитить стандартные учетные записи User и Admin от попыток их
+модификации. Сделаем новый профиль `HEROKU` и в `UserService` введем флаг `modificationRestriction` - нужна ли нам такая проверка.
+Через `Environment` проверяем активный профиль и для "Heroku" устанавливаем флаг в *true*.
+В методах, изменяющих пользователя, проверяем этот флаг и `id` изменяемой сущности, и, попытке несанкционированных изменений, бросаем `UpdateRestrictionException`.
+Отнаследовал это исключение от `ApplicationException` - универсального исключения нашего приложения, в котором можно задавать тип и код локализации ошибки.
+В `GlobalExceptionHandler` и `ExceptionInfoHandler` создаем обработчики `ApplicationException`.
+Для тестирования исключения при попытке изменение пользователя и админа в профиле "Heroku" делаем `HerokuRestControllerTest`:
+задаем профиль запуска `@ActiveProfiles(HEROKU)`, делаем модификацию и проверяем исключение.
+
+#### Apply 11_14_restrict_modification.patch
+ - В `UserService` добавил защиту от изменения `Admin/User` для профиля `HEROKU` (в `UserService` заинжектил `Environment` и сделал проверку на наличие профиля `HEROKU`)
+ - **В выпускном проекте (если только не выставляете в облако для показа) это НЕ требуется**.
+ - Чтобы тесты были рабочими, ввел профиль `HEROKU`, работающий так же, как и `POSTGRES`.
+ - Добавил универсальный `ApplicationException` для работы с ошибками с поддержкой i18n в приложении (от него отнаследовал `UpdateRestrictionException`)
+
+> Для тестирования с профилем heroku добавьте в VM options: `-Dspring.profiles.active="datajpa,heroku"`
+
+###  3. Деплой приложения в Heroku.
+
+
+ Краткое содержание
+
+>Бесплатный тариф Heroku имеет ряд ограничений:
+> - после 30 минут неактивности задеплоенное приложение останавливается, и при последующем обращении к нему придется ожидать пока оно вновь запустится.
+> - приложение будет доступно только 18 часов в сутки, 6 часов оно должно бездействовать.
+>Посмотрите на [маленькие хитрости с Heroku - активность 24/7](https://javarush.ru/groups/posts/1987-malenjhkie-khitrosti-s-heroku)
+ - [Add a ping or simple browser monitor](https://docs.newrelic.com/docs/synthetics/synthetic-monitoring/using-monitors/add-edit-monitors/#simple)
+
+ Для деплоя приложения нужно создать учетную запись на Heroku:
+ 1) Создать новый проект. При создании желательно выбирать регион, который находится ближе к Вам (будет меньше пинг)
+ 2) Связываем этот проект с репозиторием приложения на GitHub.
+ 3) Во вкладке Resources в разделе Addons найти "Heroku Postgres", выбрать Hobby Dev - Free версию и создать базу.
+ 4) Во вкладке Settings проекта появилась переменная DATABASE_URL. С помощью этой переменной подключиться к базе данных из Intellij Idea:
+ - скопировать URL базы данных
+ - добавить в Idea Datasource "PostgreSQL"
+ - если отсутствует драйвер postgres - подгрузить его
+ - из URL БД взять необходимые данные для заполнения полей user, password, host, port, database
+ - в advanced настройках установить ssl в true и настроить sslfactory
+ - выполнить Test Connection, чтобы убедиться, что настройки определены верно и БД работает.
+
+- Для работы с БД на Heroku создадим настройки `heroku.properties`, в которых отключим инициализацию
+базы при каждом запуске приложения.
+- Инициализируем удаленную базу данных, запустив на ней скрипт `initDB` и заполним ее начальными данными, выполнив `populateDB`.
+- Для деплоя на Heroku создадим файл `system.properties`, в котором будет записана версия JDK нашего приложения.
+- Приложения на Heroku будет запускать через Tomcat, для этого сконфигурируем в `pom.xml` для Maven профиля **heroku** плагин `webapp-runner`
+(пример его подключения можно найти в документации Heroku).
+- Нам нужно, чтобы при сборке Heroku собрал наш проект с Maven-профилем *heroku*: в корне проекта создадим файл `settings.xml` с этим профилем по умолчанию.
+- В `spring-db.xml` создадим еще один профиль `heroku`. Для этого профиля укажем параметры
+подключения к удаленной базе данных и расположение файла конфигурации JPA - `heroku.properties`.
+- Запуск приложения конфигурируется в *Procfile*. Укажем здесь активные профили Spring (`-Dspring.profiles.active="datajpa,heroku"`).
+
+> В проекте есть файл hr.bat - в нем указаны команды, которые эмулируют действия Heroku.
+> Его нужно запустить и посмотреть, как поведет себя приложение при деплое.
+
+Теперь можно сделать коммит и во вкладке Deploy на хероку выполнить Manual Deploy
+из ветки Master на гитхабе.
+
+> К запущенному на хероку приложению можно выполнить подключение для просмотра логов с помощью утилиты Heroku Toolbelt, которую можно установить с сайта Heroku.
+
+Если сейчас запустить задеплоенное на Heroku приложение, то можно увидеть, что не загрузились
+ресурсы локализации, потому что они находятся по пути, заданной в переменной окружения `TOPJAVA_ROOT`,
+которую мы ранее настраивали для нашей операционной системы. Зададим эту переменную в настройках
+проекта на Heroku, в разделе Config Variables. Также здесь мы можем задать переменную
+ERROR_PAGE - на нее Heroku перенаправит пользователя, если наше приложение не будет работоспособным
+в момент обращения к нему.
+
+Теперь мы повторно выполним Deploy и убедимся, что теперь все работает корректно.
+
+
+#### Apply 11_15_heroku.patch
+
+`hr.bat` запускает внутри 2 процесса - maven (`mvn` и `java`). Проверьте из консоли, что они будут работать (прописаны в системную переменную `Path`). Если запускаетесь из под IDEA и меняете `Path`, не забывайте перегрузиться.
+```
+mvn -version
+java --version
+.\hr.bat
+```
+
+> - Добавил зависимости `postgres` в профиль мавена `heroku`
+> - [Поменял настройки `dataSource` для профиля `heroku`](http://stackoverflow.com/questions/10684244/dbcp-validationquery-for-different-databases).
+При опускании/поднятии приложения в heroku.com портятся коннекты в пуле и необходимо их валидировать.
+> - В tomcat 9 в `META-INF\services\javax.cache.spi.CachingProvider` находится реализация провайдера Redis JCache, которая конфликтует с нашеим ehcache провайдером: `ehcache-3.6.1.jar!\META-INF\services\javax.cache.spi.CachingProvider`.
+ [Решается заменой зависимости на `webapp-runner-main`](https://github.com/jsimone/webapp-runner#excluding-memcached-and-redis-libraries)
+
+### Приложение деплоится в ROOT: [http://localhost:8080](http://localhost:8080)
+
+- [Деплой Java Spring приложения в PaaS-платформу Heroku](http://habrahabr.ru/post/265591)
+```
+Config Vars
+ ERROR_PAGE_URL=...
+ TOPJAVA_ROOT=/app
+
+Datasources advanced
+ ssl=true
+ sslmode=require
+ sslfactory=org.postgresql.ssl.NonValidatingFactory
+```
+#### Внимание: Heroku больше не деплоит c GitHub, [нужна деплоить через CLI](https://stackoverflow.com/a/71895325/548473)
+1. Устанавливаем локально [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli#install-the-heroku-cli)
+2. Убеждаемся, что в локальный репозиторий все запушено в ветку master [и делаем](https://help.heroku.com/CKVOUPSY/how-to-switch-deployment-method-from-github-to-heroku-git-with-all-the-changes-app-code-available-in-a-github-repo)
+`heroku git:remote -a [app-name]`
+`git push heroku master:main`
+-----------------
+- Ресурсы:
+ - PaaS-платформа Heroku
+ - Конфигурирование приложения для запуска через Tomcat-based Java Web
+ - Конфигурирование DataSource profile для Heroku
+ - Find your Platform as a Service
+ - Getting Started with Java on Heroku
+ - Managing Your SSH Keys
+ - Deploy your application to Heroku
+ - Развертывание приложений Java с помощью PaaS от Heroku
+ - A Java Developer’s Guide to PaaS
+ - A Simple PaaS Comparison Guide (With the Java Dev in Mind)
+ - Java PaaS shootout
+ - [Deploying Java Applications with the Heroku Maven Plugin](https://devcenter.heroku.com/articles/deploying-java-applications-with-the-heroku-maven-plugin)
+
+
+###  4. Собеседование. Разработка ПО
+- [Темы/ресурсы тестового собеседования](http://javaops.ru/interview/test.html)
+- [Составление резюме, подготовка к интервью, поиск работы](https://github.com/JavaOPs/topjava/blob/master/cv.md)
+- [Слайды](https://docs.google.com/presentation/d/18o__IGRqYadi4jx2wX2rX6AChHh-KrxktD8xI7bS33k), [Книги](http://javaops.ru/view/books)
+- [Jenkins/Hudson: что такое и для чего он нужен](https://habrahabr.ru/post/334730/)
+
+###  [Вебинар: Составление резюме и поиск работы в IT](https://www.facebook.com/watch/live/?v=2789025168007756)
+###  Разбор типовых собеседований (необработанный вебинар)
+###  Вебинар выпускников
+
+-----------------------
+
+##  Домашнее Задание:
+### **Задеплоить свое приложение в Heroku**
+ - [Маленькие хитрости с Heroku - активность 24/7](https://javarush.ru/groups/posts/1987-malenjhkie-khitrosti-s-heroku)
+
+### **Пройдите основы Spring Boot по курсу [BootJava](https://javaops.ru/view/bootjava)**
+- **Занятие по миграция на BootJava будет в начале следующей недели**
+
+### **[Выполнить выпускной проект](https://github.com/JavaWebinar/topjava/blob/doc/doc/graduation.md)**
+ - Сроки сдачи указан в выпускном.
+ - Если есть проверка или Диплом, после выполнения выпускного [заполни форму проверки](https://docs.google.com/forms/d/1G8cSGBfXIy9bNECo6L-tkxWQYWeVhfzR7te4b-Jwn-Q)
+ - Если проверки или Диплома нет, заполнять не нужно.
+ - **Возможно доплатить за ревью отдельно из JavaOPs профиля, как за тестовое собеседование: 3450р**
+
+### **Сделать / обновить резюме (отдать на ревью в канал #hw11 группы slack)**
+- **Вставь ссылку на свой сертификат [из личного профиля](http://javaops.ru/auth/profile#finished), немного досрочно:)**
+ - [Загрузка сайта на GitHub. Бесплатный хостинг и домен.](https://vk.com/video-58538268_456239051?list=661b165047264e7952)
+ - [CSS theme for hosting your personal site, blog, or portfolio](https://mademistakes.com/work/minimal-mistakes-jekyll-theme/)
+
+####  Замечания по резюме:
+ - **если нет опыта в IT, обязательно вставь [участие в стажировке Topjava](https://github.com/JavaOPs/topjava/blob/master/cv.md#Позиционирование-проекта-topjava). Весь не-IT опыт можно кратко.**
+ - варианты размещения: Pdf в любом облаке, [Google Doc](https://docs.google.com/), LinkedIn, HH, [еще варианты и рекомендации](https://github.com/JavaOPs/topjava/blob/master/cv.md#составление-резюме)
+Хорошо, если будет в html или pdf формате (например в https://pages.github.com/). [Например так](https://gkislin.github.io/), [на github](https://github.com/gkislin/gkislin.github.io/blob/master/index.html). Возраст и день рождения писать не обязательно
+ - [все упоминания Junior убрать!!](https://vk.com/javawebinar?w=wall-58538268_1589)
+ - линки делай кликабельными (если формат поддерживает)
+ - всю выгодную для себя информацию (и важную для HR) распологайте вверху. Название секций в резюме и их порядок относительно стандартный и важный
+ - **Резюме на hh или других ресурсах ДОЛЖНО БЫТЬ ОТКРЫТО ДЛЯ ПРОСМОТРА и иметь телефон для связи**
+ - Заполните контакты `skype/telegram/whatsapp`, HR ими пользуется! Почта как контакт очень медленная, телефон может быть не всегда удобен. Вообще `skype/telegram` для программиста - **Must have**.
+ - **Добавьте в резюме ссылки на свои проекты в `GitHub` и на задеплоенные в `Heroku`. Не забудьте про выпускной!**.
+ - Диплом РФ от Виакадемии о [профессиональной переподготовке](https://ru.wikipedia.org/wiki/Профессиональная_переподготовка) приравнивается ко второму высшему образованию. В резюме, полагаю, можно указать в высшем образовании
+ - Заполнить в [своем профиле Java Online Projects](http://javaops.ru/auth/profileER) ссылку на резюме и информацию по поиску работы (если конечно актуально): резюме, флаги рассматриваю работу, готов к релокации и информация для HR.
+ - **Рассылку обновления базы соискателей по HR буду делать в начале июня, можно не спешить**
+ - По набору на [стажировку с последующим трудоустройством в группе компаний «Лига цифровой экономики»](https://javaops.ru/view/register/registerInternship) детали будут в середине мая.
+
+### **После ревью резюме - опубликовать на ресурсах IT вакансий**
+ - [Основные сайты поиска работы](https://github.com/JavaOPs/topjava/blob/master/cv.md#основные-сайты-поиска-работы)
+
+### **Получить первое открытое занятие МНОГОПОТОЧНОСТЬ и пройти эту важную тему в [проекте Masterjava](http://javaops.ru/view/masterjava)**
+ - Обучение на MasterJava идет в индивидуальном режиме без проверки ДЗ: старт в любой момент, время прохождение ничем не ограничено
+ - Проект, патчи, группа Slack, занятия и видео, разбор ДЗ аналогичны проекту Topjava.
+
+#### Возможные доработки приложения:
+- Разделить `Meal.dateTime` на `date` и `time` и выполнять запрос целиком в SQL
+- Для редактирования паролей сделать отдельный интерфейс с запросом старого пароля и кнопку сброса пароля для администратора.
+- Добавление и удаление ролей для пользователей в админке.
+- Перевести UI на Angular / Vaadin elements /GWT /GXT /Vaadin / ZK/ [Ваш любимый фреймворк]..
+- Перевести шаблоны с JSP на Thymeleaf
+- Сделать авторизацию в приложение по OAuth 2.0 (Spring Security OAuth,
+VK auth, github oauth, ...)
+- Сделать отображение еды постранично, с поиском и сортировкой на стороне сервера.
+- Перевод проекта на https
+- Сделать desktop/mobile приложение, работающее по REST с нашим приложением.
+- Показ ошибок в модальном окне редактирования таблицы так же, как и в JSP профиля
+- Limit login attempts example
+- Сделать авторизацию REST по JWT
+
+#### Доработки участников прошлых выпусков:
+- [Авторизация в приложение по OAuth2 через GitHub](http://rblik-topjava.herokuapp.com)
+ - [GitHub, ветка oauth](https://github.com/rblik/topjava/tree/oauth)
+- [Авторизация в приложение по OAuth2 через GitHub/Facebook/Google](http://tj9.herokuapp.com)
+ - [GitHub](https://github.com/jacksn/topjava)
+- [Angular 2 UI](https://topjava-angular2.herokuapp.com)
+ - [tutorial по доработке](https://github.com/Gwulior/article/blob/master/article.md)
+ - [ветка angular2 в гитхабе](https://github.com/12ozCode/topjava08-to-angular2/tree/angular2)
+- [Отдельный фронтэнд на Angular 2, который работает по REST с авторизацией по JWT](https://topjava6-frontend.herokuapp.com)
+ - [ветка development фронтэнда](https://github.com/evgeniycheban/topjava-frontend/tree/development)
+ - [ветка development бэкэнда](https://github.com/evgeniycheban/topjava/tree/development)
+ - в JWT токенен приложение topjava передает email, name и роль admin как boolean true/false,
+на клиенте он декодируется и из него получается auth-user, с которым уже работает фронтэнд
+
+#### Жду твою доработку из списка!
+
+### Ресурсы по Проекту
+- Уроки Bootstrap 4
+- Spring at tutorialspoint
+- Articles in Spring
+- Learn Spring on Baeldung
+- Spring Framework
+ Reference Documentation
+- Hibernate Documentation
+- Java Course (книга 2)
+- Справочник «Паттерны проектирования»
+- Catalog of Patterns of Enterprise Application Architecture
diff --git a/hr.bat b/hr.bat
new file mode 100644
index 000000000..a7d83b14d
--- /dev/null
+++ b/hr.bat
@@ -0,0 +1,2 @@
+call mvn -B -s settings.xml -DskipTests=true clean package
+call java -Dspring.profiles.active="datajpa,heroku" -DDATABASE_URL="postgres://user:password@localhost:5432/topjava" -jar target/dependency/webapp-runner.jar target/*.war
diff --git a/pom.xml b/pom.xml
index e3ccaaae4..34c72eea3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,70 +9,72 @@
1.0-SNAPSHOTCalories Management
- https://javaops-demo.ru/topjava
+ http://topjava.herokuapp.com/
- 21
+ 17UTF-8UTF-8
- 5.3.39
- 5.8.16
- 2.7.18
- 2.20.1
- 9.0.111
- 1.21.2
+
+ 5.3.20
+ 2.7.1
+ 5.7.2
+
+ 2.13.3
+ 9.0.64
+
+
+ 1.2.11
+ 1.7.36
+
+
+ 42.4.0
- 5.6.15.Final
- 6.2.5.Final
+ 5.6.9.Final
+ 6.2.3.Final3.0.1-b12
- 3.10.8
+ 3.10.0
+
+
+ 5.8.2
+ 3.23.1
+ 2.2
+ 2.7.0
- 4.6.2
- 3.7.1
+ 4.6.1
+ 3.6.02.5.20-13.1.4
- 1.13.5
-
-
- 1.5.20
- 2.0.17
-
-
- 42.7.8
-
- 5.14.1
- 3.27.6
- 3.0
- 2.10.0
+ 1.11.4topjavapackage
-
- org.apache.maven.plugins
- maven-war-plugin
- 3.4.0
- org.apache.maven.pluginsmaven-compiler-plugin
- 3.14.1
+ 3.8.1${java.version}${java.version}
+
+ org.apache.maven.plugins
+ maven-war-plugin
+ 3.3.2
+ org.apache.maven.pluginsmaven-surefire-plugin
- 3.5.4
+ 2.22.2-Dfile.encoding=UTF-8
@@ -83,7 +85,7 @@
org.codehaus.cargocargo-maven3-plugin
- 1.10.24
+ 1.9.13tomcat9x
@@ -128,6 +130,7 @@
org.slf4jslf4j-api${slf4j.version}
+ compile
@@ -137,14 +140,6 @@
runtime
-
-
- com.google.code.findbugs
- annotations
- 3.0.1
- compile
-
-
javax.annotationjavax.annotation-api
@@ -206,7 +201,7 @@
org.jsoupjsoup
- ${jsoup.version}
+ 1.14.3
@@ -332,7 +327,7 @@
org.junit.jupiterjunit-jupiter-engine
- ${junit.version}
+ ${junit.jupiter.version}test
@@ -353,6 +348,7 @@
spring-testtest
+
org.springframework.securityspring-security-test
@@ -371,7 +367,7 @@
org.junit.platformjunit-platform-launcher
- 1.14.1
+ 1.8.2test
@@ -383,7 +379,7 @@
org.hsqldbhsqldb
- 2.7.4
+ 2.6.1
@@ -413,6 +409,50 @@
true
+
+ heroku
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.3.0
+
+
+ package
+
+ copy
+
+
+
+
+
+ com.heroku
+ webapp-runner-main
+ 9.0.52.1
+ webapp-runner.jar
+
+
+
+
+
+
+
+
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+
+
+ org.apache.tomcat
+ tomcat-jdbc
+ ${tomcat.version}
+
+
+
@@ -426,4 +466,4 @@
-
\ No newline at end of file
+
diff --git a/settings.xml b/settings.xml
new file mode 100644
index 000000000..9681d7232
--- /dev/null
+++ b/settings.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ heroku
+
+
diff --git a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java
index b51930e87..2126d687b 100644
--- a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java
+++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java
@@ -2,7 +2,7 @@
import ru.javawebinar.topjava.model.User;
import ru.javawebinar.topjava.to.UserTo;
-import ru.javawebinar.topjava.util.UsersUtil;
+import ru.javawebinar.topjava.util.UserUtil;
import java.io.Serial;
@@ -14,7 +14,7 @@ public class AuthorizedUser extends org.springframework.security.core.userdetail
public AuthorizedUser(User user) {
super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles());
- setTo(UsersUtil.asTo(user));
+ setTo(UserUtil.asTo(user));
}
public int getId() {
diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java
index 80dc6ef11..19dc1d13a 100644
--- a/src/main/java/ru/javawebinar/topjava/Profiles.java
+++ b/src/main/java/ru/javawebinar/topjava/Profiles.java
@@ -13,9 +13,9 @@ public class Profiles {
public static final String
POSTGRES_DB = "postgres",
HSQL_DB = "hsqldb",
- VDS = "vds";
+ HEROKU = "heroku";
- // Get DB profile depending on DB driver in classpath
+ // Get DB profile depending of DB driver in classpath
public static String getActiveDbProfile() {
if (ClassUtils.isPresent("org.postgresql.Driver", null)) {
return POSTGRES_DB;
diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java
similarity index 81%
rename from src/test/java/ru/javawebinar/topjava/SpringMain.java
rename to src/main/java/ru/javawebinar/topjava/SpringMain.java
index a9e46b208..1aa3df136 100644
--- a/src/test/java/ru/javawebinar/topjava/SpringMain.java
+++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java
@@ -1,7 +1,6 @@
package ru.javawebinar.topjava;
-import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.springframework.context.support.GenericXmlApplicationContext;
import ru.javawebinar.topjava.model.Role;
import ru.javawebinar.topjava.model.User;
import ru.javawebinar.topjava.to.MealTo;
@@ -20,7 +19,11 @@
public class SpringMain {
public static void main(String[] args) {
// java 7 automatic resource management (ARM)
- try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/inmemory.xml")) {
+ try (GenericXmlApplicationContext appCtx = new GenericXmlApplicationContext()) {
+ appCtx.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION);
+ appCtx.load("spring/inmemory.xml");
+ appCtx.refresh();
+
System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames()));
AdminRestController adminUserController = appCtx.getBean(AdminRestController.class);
adminUserController.create(new User(null, "userName", "email@mail.ru", "password", 2000, Role.ADMIN));
diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java
index ad5ded601..0d22c4938 100644
--- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java
+++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java
@@ -1,12 +1,12 @@
package ru.javawebinar.topjava.model;
import io.swagger.annotations.ApiModelProperty;
+import org.hibernate.Hibernate;
+import org.springframework.util.Assert;
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)
@@ -42,19 +42,23 @@ public Integer getId() {
@Override
public String toString() {
- return getClass().getSimpleName() + ":" + getId();
+ return getClass().getSimpleName() + ":" + id;
}
- // 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());
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || !getClass().equals(Hibernate.getClass(o))) {
+ return false;
+ }
+ AbstractBaseEntity that = (AbstractBaseEntity) o;
+ return id != null && id.equals(that.id);
}
@Override
- public final int hashCode() {
- return getClassWithoutInitializingProxy(this).hashCode();
+ public int hashCode() {
+ return id == null ? 0 : id;
}
}
\ 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
index 50a2c1d6a..9583bb8c6 100644
--- a/src/main/java/ru/javawebinar/topjava/model/Meal.java
+++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java
@@ -28,7 +28,7 @@
// "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")})
+@Table(name = "meals", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meals_unique_user_datetime_idx")})
public class Meal extends AbstractBaseEntity {
public static final String ALL_SORTED = "Meal.getAll";
public static final String DELETE = "Meal.delete";
diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java
similarity index 100%
rename from src/main/java/ru/javawebinar/topjava/to/MealTo.java
rename to src/main/java/ru/javawebinar/topjava/model/MealTo.java
diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java
index 41cbd5db8..3bb6d4e24 100644
--- a/src/main/java/ru/javawebinar/topjava/model/User.java
+++ b/src/main/java/ru/javawebinar/topjava/model/User.java
@@ -22,7 +22,7 @@
import javax.validation.constraints.Size;
import java.util.*;
-import static ru.javawebinar.topjava.util.UsersUtil.DEFAULT_CALORIES_PER_DAY;
+import static ru.javawebinar.topjava.util.UserUtil.DEFAULT_CALORIES_PER_DAY;
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@NamedQueries({
@@ -62,11 +62,11 @@ public class User extends AbstractNamedEntity implements HasIdAndEmail {
@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")})
+ @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"),
+ uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_roles")})
@Column(name = "role")
@ElementCollection(fetch = FetchType.EAGER)
- // @Fetch(FetchMode.SUBSELECT)
+// @Fetch(FetchMode.SUBSELECT)
@BatchSize(size = 200)
@JoinColumn
@OnDelete(action = OnDeleteAction.CASCADE)
@@ -90,7 +90,7 @@ public User(User u) {
}
public User(Integer id, String name, String email, String password, int caloriesPerDay, Role... roles) {
- this(id, name, email, password, caloriesPerDay, true, new Date(), List.of(roles));
+ this(id, name, email, password, caloriesPerDay, true, new Date(), Arrays.asList((roles)));
}
public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) {
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java
index 924a2b781..e2e6e1c76 100644
--- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java
@@ -30,7 +30,7 @@ public class JdbcMealRepository implements MealRepository {
public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.insertMeal = new SimpleJdbcInsert(jdbcTemplate)
- .withTableName("meal")
+ .withTableName("meals")
.usingGeneratedKeyColumns("id");
this.jdbcTemplate = jdbcTemplate;
@@ -54,7 +54,7 @@ public Meal save(Meal meal, int userId) {
meal.setId(newId.intValue());
} else {
if (namedParameterJdbcTemplate.update("" +
- "UPDATE meal " +
+ "UPDATE meals " +
" SET description=:description, calories=:calories, date_time=:date_time " +
" WHERE id=:id AND user_id=:user_id", map) == 0) {
return null;
@@ -66,26 +66,26 @@ public Meal save(Meal meal, int userId) {
@Override
@Transactional
public boolean delete(int id, int userId) {
- return jdbcTemplate.update("DELETE FROM meal WHERE id=? AND user_id=?", id, userId) != 0;
+ return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0;
}
@Override
public Meal get(int id, int userId) {
List meals = jdbcTemplate.query(
- "SELECT * FROM meal WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId);
+ "SELECT * FROM meals WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId);
return DataAccessUtils.singleResult(meals);
}
@Override
public List getAll(int userId) {
return jdbcTemplate.query(
- "SELECT * FROM meal WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId);
+ "SELECT * FROM meals WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId);
}
@Override
public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
return jdbcTemplate.query(
- "SELECT * FROM meal WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC",
+ "SELECT * FROM meals WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC",
ROW_MAPPER, userId, startDateTime, endDateTime);
}
}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java
index 4074ff2c7..bb7df590c 100644
--- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java
@@ -90,7 +90,7 @@ 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 -> {
+ jdbcTemplate.query("SELECT * FROM user_roles", rs -> {
map.computeIfAbsent(rs.getInt("user_id"), userId -> EnumSet.noneOf(Role.class))
.add(Role.valueOf(rs.getString("role")));
});
@@ -101,7 +101,7 @@ public List getAll() {
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(),
+ jdbcTemplate.batchUpdate("INSERT INTO user_roles (user_id, role) VALUES (?, ?)", roles, roles.size(),
(ps, role) -> {
ps.setInt(1, u.id());
ps.setString(2, role.name());
@@ -110,12 +110,12 @@ private void insertRoles(User u) {
}
private void deleteRoles(User u) {
- jdbcTemplate.update("DELETE FROM user_role WHERE user_id=?", u.getId());
+ jdbcTemplate.update("DELETE FROM user_roles 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());
+ List roles = jdbcTemplate.queryForList("SELECT role FROM user_roles 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
index 6df9fd99f..300a920ae 100644
--- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java
+++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java
@@ -25,8 +25,10 @@ public Meal save(Meal meal, int userId) {
if (meal.isNew()) {
em.persist(meal);
return meal;
+ } else if (get(meal.id(), userId) == null) {
+ return null;
}
- return get(meal.id(), userId) == null ? null : em.merge(meal);
+ return em.merge(meal);
}
@Override
diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java
index b4ebae213..29f1d3613 100644
--- a/src/main/java/ru/javawebinar/topjava/service/MealService.java
+++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java
@@ -11,7 +11,7 @@
import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfDayOrMin;
import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfNextDayOrMax;
-import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkNotFound;
+import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkNotFoundWithId;
@Service
public class MealService {
@@ -23,11 +23,11 @@ public MealService(MealRepository repository) {
}
public Meal get(int id, int userId) {
- return checkNotFound(repository.get(id, userId), id);
+ return checkNotFoundWithId(repository.get(id, userId), id);
}
public void delete(int id, int userId) {
- checkNotFound(repository.delete(id, userId), id);
+ checkNotFoundWithId(repository.delete(id, userId), id);
}
public List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) {
@@ -40,7 +40,7 @@ public List getAll(int userId) {
public void update(Meal meal, int userId) {
Assert.notNull(meal, "meal must not be null");
- checkNotFound(repository.save(meal, userId), meal.id());
+ checkNotFoundWithId(repository.save(meal, userId), meal.id());
}
public Meal create(Meal meal, int userId) {
@@ -49,6 +49,6 @@ public Meal create(Meal meal, int userId) {
}
public Meal getWithUser(int id, int userId) {
- return checkNotFound(repository.getWithUser(id, userId), id);
+ return checkNotFoundWithId(repository.getWithUser(id, userId), id);
}
}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java
index e4aaed9ea..677a937bb 100644
--- a/src/main/java/ru/javawebinar/topjava/service/UserService.java
+++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java
@@ -18,13 +18,14 @@
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 ru.javawebinar.topjava.util.UserUtil;
import ru.javawebinar.topjava.util.exception.UpdateRestrictionException;
import java.util.List;
-import static ru.javawebinar.topjava.util.UsersUtil.prepareToSave;
+import static ru.javawebinar.topjava.util.UserUtil.prepareToSave;
import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkNotFound;
+import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkNotFoundWithId;
@Service("userService")
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@@ -38,7 +39,7 @@ public class UserService implements UserDetailsService {
@Autowired
@SuppressWarnings("deprecation")
public void setEnvironment(Environment environment) {
- modificationRestriction = environment.acceptsProfiles(Profiles.VDS);
+ modificationRestriction = environment.acceptsProfiles(Profiles.HEROKU);
}
public UserService(UserRepository repository, PasswordEncoder passwordEncoder) {
@@ -55,11 +56,11 @@ public User create(User user) {
@CacheEvict(value = "users", allEntries = true)
public void delete(int id) {
checkModificationAllowed(id);
- checkNotFound(repository.delete(id), id);
+ checkNotFoundWithId(repository.delete(id), id);
}
public User get(int id) {
- return checkNotFound(repository.get(id), id);
+ return checkNotFoundWithId(repository.get(id), id);
}
public User getByEmail(String email) {
@@ -75,18 +76,17 @@ public List 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
+// checkNotFoundWithId : check works only for JDBC, disabled
checkModificationAllowed(user.id());
prepareAndSave(user);
}
-
@CacheEvict(value = "users", allEntries = true)
@Transactional
public void update(UserTo userTo) {
checkModificationAllowed(userTo.id());
User user = get(userTo.id());
- prepareAndSave(UsersUtil.updateFromTo(user, userTo));
+ prepareAndSave(UserUtil.updateFromTo(user, userTo));
}
@CacheEvict(value = "users", allEntries = true)
@@ -112,7 +112,7 @@ private User prepareAndSave(User user) {
}
public User getWithMeals(int id) {
- return checkNotFound(repository.getWithMeals(id), id);
+ return checkNotFoundWithId(repository.getWithMeals(id), id);
}
protected void checkModificationAllowed(int id) {
diff --git a/src/main/java/ru/javawebinar/topjava/to/BaseTo.java b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java
index 7ccb8d970..b7a7de6b7 100644
--- a/src/main/java/ru/javawebinar/topjava/to/BaseTo.java
+++ b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java
@@ -1,10 +1,8 @@
package ru.javawebinar.topjava.to;
-import io.swagger.annotations.ApiModelProperty;
import ru.javawebinar.topjava.HasId;
public abstract class BaseTo implements HasId {
- @ApiModelProperty(hidden = true)
protected Integer id;
public BaseTo() {
diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java
index 4fc5aec92..bf5296d69 100644
--- a/src/main/java/ru/javawebinar/topjava/to/UserTo.java
+++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java
@@ -2,8 +2,8 @@
import org.hibernate.validator.constraints.Range;
import ru.javawebinar.topjava.HasIdAndEmail;
-import ru.javawebinar.topjava.util.UsersUtil;
import ru.javawebinar.topjava.util.validation.NoHtml;
+import ru.javawebinar.topjava.util.UserUtil;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@@ -33,7 +33,7 @@ public class UserTo extends BaseTo implements HasIdAndEmail, Serializable {
@Range(min = 10, max = 10000)
@NotNull
- private Integer caloriesPerDay = UsersUtil.DEFAULT_CALORIES_PER_DAY;
+ private Integer caloriesPerDay = UserUtil.DEFAULT_CALORIES_PER_DAY;
public UserTo() {
}
diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
index 09052faa5..1fb662b11 100644
--- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
+++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
@@ -7,6 +7,7 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
public class DateTimeUtil {
public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm";
@@ -24,7 +25,7 @@ public static LocalDateTime atStartOfDayOrMin(LocalDate localDate) {
}
public static LocalDateTime atStartOfNextDayOrMax(LocalDate localDate) {
- return localDate != null ? localDate.plusDays(1).atStartOfDay() : MAX_DATE;
+ return localDate != null ? localDate.plus(1, ChronoUnit.DAYS).atStartOfDay() : MAX_DATE;
}
public static String toString(LocalDateTime ldt) {
diff --git a/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java
similarity index 97%
rename from src/main/java/ru/javawebinar/topjava/util/UsersUtil.java
rename to src/main/java/ru/javawebinar/topjava/util/UserUtil.java
index bdb4291f7..a0700dd49 100644
--- a/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java
+++ b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java
@@ -5,7 +5,7 @@
import ru.javawebinar.topjava.model.User;
import ru.javawebinar.topjava.to.UserTo;
-public class UsersUtil {
+public class UserUtil {
public static final int DEFAULT_CALORIES_PER_DAY = 2000;
diff --git a/src/main/java/ru/javawebinar/topjava/util/validation/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/validation/ValidationUtil.java
index 12840a142..6b0384c35 100644
--- a/src/main/java/ru/javawebinar/topjava/util/validation/ValidationUtil.java
+++ b/src/main/java/ru/javawebinar/topjava/util/validation/ValidationUtil.java
@@ -34,12 +34,12 @@ public static void validate(T bean) {
}
}
- public static T checkNotFound(T object, int id) {
- checkNotFound(object != null, id);
+ public static T checkNotFoundWithId(T object, int id) {
+ checkNotFoundWithId(object != null, id);
return object;
}
- public static void checkNotFound(boolean found, int id) {
+ public static void checkNotFoundWithId(boolean found, int id) {
checkNotFound(found, "id=" + id);
}
@@ -54,7 +54,7 @@ public static void checkNotFound(boolean found, String msg) {
}
}
- public static void checkIsNew(HasId bean) {
+ public static void checkNew(HasId bean) {
if (!bean.isNew()) {
throw new IllegalRequestDataException(bean + " must be new (id=null)");
}
diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java
index 543595bd2..39ec6495f 100644
--- a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java
+++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java
@@ -31,7 +31,7 @@ public class ExceptionInfoHandler {
private static final Map CONSTRAINTS_I18N_MAP = Map.of(
"users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL,
- "meal_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME);
+ "meals_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME);
private final MessageSourceAccessor messageSourceAccessor;
diff --git a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java
index 15448ce8c..bc4409869 100644
--- a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java
+++ b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java
@@ -7,12 +7,15 @@
import java.time.format.DateTimeFormatter;
import java.util.Locale;
+import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate;
+import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime;
+
public class DateTimeFormatters {
public static class LocalDateFormatter implements Formatter {
@Override
public LocalDate parse(String text, Locale locale) {
- return LocalDate.parse(text);
+ return parseLocalDate(text);
}
@Override
@@ -25,7 +28,7 @@ public static class LocalTimeFormatter implements Formatter {
@Override
public LocalTime parse(String text, Locale locale) {
- return LocalTime.parse(text);
+ return parseLocalTime(text);
}
@Override
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
index 63d9f9415..2a8b6c204 100644
--- a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java
@@ -15,7 +15,7 @@
import java.util.List;
import static ru.javawebinar.topjava.util.validation.ValidationUtil.assureIdConsistent;
-import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkIsNew;
+import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkNew;
public abstract class AbstractMealController {
private final Logger log = LoggerFactory.getLogger(getClass());
@@ -44,7 +44,7 @@ public List getAll() {
public Meal create(Meal meal) {
int userId = SecurityUtil.authUserId();
log.info("create {} for user {}", meal, userId);
- checkIsNew(meal);
+ checkNew(meal);
return service.create(meal, userId);
}
diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
index 0d3eb7e83..f9f1f5027 100644
--- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
@@ -67,4 +67,4 @@ public List getBetween(
@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
index 77a09cdc6..6bc142322 100644
--- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -8,12 +8,12 @@
import ru.javawebinar.topjava.model.User;
import ru.javawebinar.topjava.service.UserService;
import ru.javawebinar.topjava.to.UserTo;
-import ru.javawebinar.topjava.util.UsersUtil;
+import ru.javawebinar.topjava.util.UserUtil;
import java.util.List;
import static ru.javawebinar.topjava.util.validation.ValidationUtil.assureIdConsistent;
-import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkIsNew;
+import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkNew;
public abstract class AbstractUserController {
protected final Logger log = LoggerFactory.getLogger(getClass());
@@ -41,13 +41,13 @@ public User get(int id) {
public User create(UserTo userTo) {
log.info("create {}", userTo);
- checkIsNew(userTo);
- return service.create(UsersUtil.createNewFromTo(userTo));
+ checkNew(userTo);
+ return service.create(UserUtil.createNewFromTo(userTo));
}
public User create(User user) {
log.info("create {}", user);
- checkIsNew(user);
+ checkNew(user);
return service.create(user);
}
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java
index af939854a..13d50695c 100644
--- a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java
@@ -36,6 +36,7 @@ public void delete(@PathVariable int id) {
}
@PostMapping
+ @ResponseStatus(HttpStatus.NO_CONTENT)
public void createOrUpdate(@Validated(View.Web.class) UserTo userTo) {
if (userTo.isNew()) {
super.create(userTo);
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java b/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java
index d72b8aa5c..9a90091b7 100644
--- a/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java
+++ b/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java
@@ -39,7 +39,7 @@ public void validate(Object target, Errors errors) {
Assert.notNull(request, "HttpServletRequest missed");
if (request.getMethod().equals("PUT") || (request.getMethod().equals("POST") && user.getId() != null)) { // update for REST(PUT) and UI(POST)
int dbId = dbUser.id();
- // it is ok, if update ourselves
+ // it is ok, if update ourself
if (user.getId() != null && dbId == user.id()) return;
// workaround for update with user.id=null in request body
diff --git a/src/main/resources/db/heroku.properties b/src/main/resources/db/heroku.properties
new file mode 100644
index 000000000..c8146ba6f
--- /dev/null
+++ b/src/main/resources/db/heroku.properties
@@ -0,0 +1,5 @@
+jpa.showSql=false
+hibernate.format_sql=false
+hibernate.use_sql_comments=false
+database.init=false
+jdbc.initLocation=initDB.sql
\ No newline at end of file
diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql
index 4bf3d8446..7644dc610 100644
--- a/src/main/resources/db/initDB.sql
+++ b/src/main/resources/db/initDB.sql
@@ -1,5 +1,5 @@
-DROP TABLE IF EXISTS user_role;
-DROP TABLE IF EXISTS meal;
+DROP TABLE IF EXISTS user_roles;
+DROP TABLE IF EXISTS meals;
DROP TABLE IF EXISTS users;
DROP SEQUENCE IF EXISTS global_seq;
@@ -17,7 +17,7 @@ CREATE TABLE users
);
CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
-CREATE TABLE user_role
+CREATE TABLE user_roles
(
user_id INTEGER NOT NULL,
role VARCHAR NOT NULL,
@@ -25,7 +25,7 @@ CREATE TABLE user_role
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
-CREATE TABLE meal
+CREATE TABLE meals
(
id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
user_id INTEGER NOT NULL,
@@ -34,4 +34,4 @@ CREATE TABLE meal
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
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals (user_id, date_time);
\ No newline at end of file
diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql
index 9e0e195e6..f2bb54b1e 100644
--- a/src/main/resources/db/initDB_hsql.sql
+++ b/src/main/resources/db/initDB_hsql.sql
@@ -1,5 +1,5 @@
-DROP TABLE user_role IF EXISTS;
-DROP TABLE meal IF EXISTS;
+DROP TABLE user_roles IF EXISTS;
+DROP TABLE meals IF EXISTS;
DROP TABLE users IF EXISTS;
DROP SEQUENCE global_seq IF EXISTS;
@@ -18,7 +18,7 @@ CREATE TABLE users
CREATE UNIQUE INDEX users_unique_email_idx
ON USERS (email);
-CREATE TABLE user_role
+CREATE TABLE user_roles
(
user_id INTEGER NOT NULL,
role VARCHAR(255) NOT NULL,
@@ -26,7 +26,7 @@ CREATE TABLE user_role
FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE
);
-CREATE TABLE meal
+CREATE TABLE meals
(
id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY,
date_time TIMESTAMP NOT NULL,
@@ -35,5 +35,5 @@ CREATE TABLE meal
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
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx
+ ON meals (user_id, date_time)
\ No newline at end of file
diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql
index 8d66cc0e5..8265d3655 100644
--- a/src/main/resources/db/populateDB.sql
+++ b/src/main/resources/db/populateDB.sql
@@ -1,5 +1,5 @@
-DELETE FROM user_role;
-DELETE FROM meal;
+DELETE FROM user_roles;
+DELETE FROM meals;
DELETE FROM users;
ALTER SEQUENCE global_seq RESTART WITH 100000;
@@ -8,12 +8,12 @@ VALUES ('User', 'user@yandex.ru', '{noop}password', 2005),
('Admin', 'admin@gmail.com', '{noop}admin', 1900),
('Guest', 'guest@gmail.com', '{noop}guest', 2000);
-INSERT INTO user_role (role, user_id)
+INSERT INTO user_roles (role, user_id)
VALUES ('USER', 100000),
('ADMIN', 100001),
('USER', 100001);
-INSERT INTO meal (date_time, description, calories, user_id)
+INSERT INTO meals (date_time, description, calories, user_id)
VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000),
('2020-01-30 13:00:00', 'Обед', 1000, 100000),
('2020-01-30 20:00:00', 'Ужин', 500, 100000),
@@ -22,4 +22,4 @@ VALUES ('2020-01-30 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);
+ ('2020-01-31 21:00:00', 'Админ ужин', 1500, 100001);
\ No newline at end of file
diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties
index c56854a9b..ba40447d4 100644
--- a/src/main/resources/db/postgres.properties
+++ b/src/main/resources/db/postgres.properties
@@ -1,3 +1,7 @@
+#database.url=jdbc:postgresql://ec2-34-248-169-69.eu-west-1.compute.amazonaws.com:5432/d1ohm99dookbqn?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl.NonValidatingFactory
+#database.username=qhazsiozndzrzc
+#database.password=749f7852a65b5ec57bde033af8fde7f8b782a3ef802921acd4613b133d62559e
+
database.url=jdbc:postgresql://localhost:5432/topjava
database.username=user
database.password=password
diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties
index e11f0725f..2e073681a 100644
--- a/src/main/resources/db/tomcat.properties
+++ b/src/main/resources/db/tomcat.properties
@@ -1,5 +1,5 @@
database.init=false
-jdbc.initLocation=classpath:db/initDB.sql
+jdbc.initLocation=initDB.sql
jpa.showSql=true
hibernate.format_sql=true
hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index e2b565616..ab4cfe51e 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -30,4 +30,4 @@
-
\ No newline at end of file
+
diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml
index 52c5d0df3..51f5ed851 100644
--- a/src/main/resources/spring/spring-db.xml
+++ b/src/main/resources/spring/spring-db.xml
@@ -35,15 +35,6 @@
-
-
-
-
-
-
-
-
-
--->
diff --git a/system.properties b/system.properties
new file mode 100644
index 000000000..0dc726cec
--- /dev/null
+++ b/system.properties
@@ -0,0 +1 @@
+java.runtime.version=17
\ No newline at end of file