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 @@ -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/bee16f3145654047a0505c62aeefd8a2)](https://app.codacy.com/gh/JavaWebinar/topjava/dashboard) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/bee16f3145654047a0505c62aeefd8a2)](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 + +## Материалы занятия + +### ![correction](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правки в проекте + +#### 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` + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW6 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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/) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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) + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> Зачем у нас и 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 или нет. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание 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. Если у вас там пробелы, пройдите его основы** + +--------------------- + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки и подсказки по реализации + +- 1: Ошибка в тесте _Invalid read array from JSON_ обычно расшифровывается немного ниже: читайте внимательно. +- 2: Jackson и неизменяемые объекты (для сериализации MealTo) +- 3: Если у meal, приходящий в контроллер, поля `null`, проверьте `@RequestBody` перед параметром (данные приходят в формате JSON) +- 4: При проблемах с собственным форматтером убедитесь, что в конфигурации `Topjava + +## Материалы занятия + +- **Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере (в хроме `Ctrl+F5`)** +- **При удалении файлов не забывайте делать clean: `mvn clean`** + +### ![correction](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правки в проекте + +#### Apply 8_0_fix.patch +Время еды приходит с UI с точностью до минут + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW7 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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). + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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`. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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 + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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) + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> А где реально этот путь "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 + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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"` + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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) + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 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/) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Вебинар: Составление резюме и поиск работы в IT](https://www.facebook.com/watch/live/?v=2789025168007756) +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Разбор типовых собеседований (необработанный вебинар) +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Вебинар выпускников + +----------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее Задание: +### **Задеплоить свое приложение в 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/) + +#### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания по резюме: + - **если нет опыта в 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-SNAPSHOT Calories Management - https://javaops-demo.ru/topjava + http://topjava.herokuapp.com/ - 21 + 17 UTF-8 UTF-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.Final 3.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.0 2.5.20-1 3.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.4 topjava package - - org.apache.maven.plugins - maven-war-plugin - 3.4.0 - org.apache.maven.plugins maven-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.plugins maven-surefire-plugin - 3.5.4 + 2.22.2 -Dfile.encoding=UTF-8 @@ -83,7 +85,7 @@ org.codehaus.cargo cargo-maven3-plugin - 1.10.24 + 1.9.13 tomcat9x @@ -128,6 +130,7 @@ org.slf4j slf4j-api ${slf4j.version} + compile @@ -137,14 +140,6 @@ runtime - - - com.google.code.findbugs - annotations - 3.0.1 - compile - - javax.annotation javax.annotation-api @@ -206,7 +201,7 @@ org.jsoup jsoup - ${jsoup.version} + 1.14.3 @@ -332,7 +327,7 @@ org.junit.jupiter junit-jupiter-engine - ${junit.version} + ${junit.jupiter.version} test @@ -353,6 +348,7 @@ spring-test test + org.springframework.security spring-security-test @@ -371,7 +367,7 @@ org.junit.platform junit-platform-launcher - 1.14.1 + 1.8.2 test @@ -383,7 +379,7 @@ org.hsqldb hsqldb - 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