diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000000..745a0334068e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +# https://docs.travis-ci.com/user/languages/java/ +language: java +jdk: openjdk11 + +#https://dzone.com/articles/travis-ci-tutorial-java-projects +cache: + directories: + - $HOME/.m2 + +# https://docs.travis-ci.com/user/database-setup/#PostgreSQL +before_script: +- psql -c 'create database topjava' -U postgres +- psql -c 'create user "user"; grant all privileges on database topjava to "user"' -U postgres + +# https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches +branches: + only: + - master + +#https://docs.travis-ci.com/user/environment-variables#Default-Environment-Variables +env: +- TOPJAVA_ROOT=$TRAVIS_BUILD_DIR + +# https://docs.travis-ci.com/user/notifications#Configuring-email-notifications +#notifications: +# email: false \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 000000000000..4afeb7900918 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java $JAVA_OPTS -Dspring.profiles.active="datajpa,heroku" -DTOPJAVA_ROOT="." -jar target/dependency/webapp-runner.jar --port $PORT target/*.war \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000000..d6e1d486e3b9 --- /dev/null +++ b/README.md @@ -0,0 +1,201 @@ +Java Enterprise Online Project +=============================== +Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), datatables, jQuery + plugins, Java 8 Stream and Time API и хранением в базах данных Postgresql и HSQLDB. + +![topjava_structure](https://user-images.githubusercontent.com/13649199/27433714-8294e6fe-575e-11e7-9c41-7f6e16c5ebe5.jpg) + + Когда вы слышите что-то, вы забываете это. + Когда вы видите что-то, вы запоминаете это. + Но только когда вы начинаете делать это, + вы начинаете понимать это + + Старинная китайская поговорка + +## Описание и план проекта +### Демо разрабатываемого приложения +### [Изменения проекта (Release Notes)](ReleaseNotes.md) +### Требования к участникам, Wiki +### Составление резюме, подготовка к интервью, поиск работы + +Вводное занятие (обязательно смотреть все видео) +=============== +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Осваиваем Java Enterprise. Трудоустройство. Ответы на вопросы. +- Слайды презентации +- Java Tools and Technologies Landscape Report 2016 +- [Java in 2017 Survey](http://www.baeldung.com/java-in-2017) +- Из юниоров в разработчики: получаем первую работу + +#### Spring Pet-Clinic +- Spring PetClinic Sample Application +- Presentation + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Системы управления версиями. Git. +- **Wiki по ведению проекта в Git** +- Система управления версиями. VCS/DVSC. +- Ресурсы: + - Интерактивная Git обучалка + - Еще одна интерактивная обучалка, по-русски + - Книга Git + - Working with remote repositories + - Видео по обучению Git + - Git Overview + - [Основы Git за 20 минут](https://www.youtube.com/watch?v=TMeZGvtQnT8) + - [Git - для новичков](https://www.youtube.com/watch?list=PLY4rE9dstrJyTdVJpv7FibSaXB4BHPInb&v=PEKN8NtBDQ0) + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Работа с проектом (выполнять инструкции) +**ВНИМАНИЕ: выбирайте для проекта простой пусть без пробелов и русских букв, например (Windows) `c:\projects\topjava\`. Иначе впоследствии будут проблемы** +> Проект постоянно улучшается, поэтому видео иногда отличается от кода проекта. Изменения указываю после видео: в `UserMeals/UserMealWithExceed` поля изменились на `private` +- **Prepare_ to_ HW0.patch (скачать и положить в каталог вашего проекта)** + +## Инструкция по шагам (из видео): +- Установить ПО (git, JDK8, IntelliJ IDEA, Maven) +- Создать аккаунт на GitHub +- Сделать Fork **ЭТОГО** проекта (https://github.com/JavaOPs/topjava) +- Сделать локальный репозиторий проекта: +
git clone https://github.com/[Ваш аккаунт]/topjava.git
+- Открыть и настроить проект в IDEA + - Выставить кодировку UTF-8 в консоли + - Поставить кодировку UTF-8 + - Поменять фонт по умолчанию (DejaVu) +- По ходу видео сделать Apply Patch... скаченного патча Prepare_ to_ HW0.patch +- Закоммитить и запушить изменения (commit + push) +- Сделать ветку домашнего задания +- Выполнить задание и залить на GitHub (commit + push) +- Переключиться в основную ветку проекта master. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW0 +``` +Реализовать метод UserMealsUtil.getFilteredWithExceeded через циклы (`forEach`): +- должны возвращаться только записи между startTime и endTime +- поле UserMealWithExceed.exceed должно показывать, + превышает ли сумма калорий за весь день параметра метода caloriesPerDay + +Т.е UserMealWithExceed - это запись одной еды, но поле exceeded будет одинаково для всех записей за этот день. + +- Проверьте результат выполнения ДЗ (можно проверить логику в http://topjava.herokuapp.com , список еды) +- Оцените Time complexity алгоритма. Если она больше O(N), например O(N*N) или N*log(N), сделайте O(N). +``` +- Java 8 Date and Time API +- Алгоритмы и структуры данных для начинающих: сложность алгоритмов +- Time complexity +- Временная сложность алгоритма +- Вычислительная сложность + +#### ВНИМАНИЕ: варианты Optional делайте в `UserMealsUtil` в одной ветке в разных методах. Проще делать, проще проверять + +#### Optional (Java 8 Stream API) +``` +Сделать реализацию через Java 8 Stream API. +``` +- Видео: Доступно о Java 8 Lambda +- Java 8: Lambda выражения +- Java 8: Потоки +- Pуководство по Java 8 Stream +- Java 8 Stream API в картинках и примерах +- [7 способов использовать groupingBy в Stream API](https://habrahabr.ru/post/348536) +- Лямбда-выражения в Java 8 +- A Guide to Java 8 +- Шпаргалка Java Stream API +- Алексея Владыкин: Элементы функционального программирования в Java +- Yakov Fain о новом в Java 8 +- stream.map vs forEach +- Дополнительно + - [Сергей Куксенко — Stream API, часть 1](https://www.youtube.com/watch?v=O8oN4KSZEXE) + - [Сергей Куксенко — Stream API, часть 2](https://www.youtube.com/watch?v=i0Jr2l3jrDA) + +#### Optional 2 (+5 бонусов) +``` +Сделать реализацию со сложностью O(N): +- циклом за 1 проход по List. Обратите внимание на п.13 замечаний +- через Stream API за 1 проход по исходному списку Stream meals + - возможно дополнительные проходы по частям списка + - нельзя использовать внешние коллекции, не являющиеся частью коллектора или результатами работы stream +``` +#### Замечания по использованию Stream API: +- Когда встречаешь что-то непривычное, приходится перестраивать мозги. Например, переход с процедурного на ООП программирование дается непросто. Те, кто не знает шаблонов (и не хотят учить) также их встречают плохо. Хорошая новость в том, что если это принять и начать использовать, то начинаешь получать от этого удовольствие. И тут главное не впасть в другую крайность: + - [Используйте Stream API проще (или не используйте вообще)](https://habrahabr.ru/post/337350/) +- Если вас беспокоить производительность стримов, обязательно прочитайте про оптимизацию + - ["Что? Где? Когда?"](http://optimization.guide/intro.html) + - [Перформанс: что в имени тебе моём?](https://habrahabr.ru/company/jugru/blog/338732/) + - [Performance это праздник](https://habrahabr.ru/post/326242/) + +При использовании Stream API производительность улучшиться только на больших задачах, где возможно распараллеливание. +Еще - просто так запустить и померять скорость JVM нельзя (как минимум дать прогреться и запустить очень большое число раз). Лучше использовать какие-нибудь бенчмарки, например [JMH](http://tutorials.jenkov.com/java-performance/jmh.html), который мы юзаем на другом проекте (Mastejava). + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания к HW0 +- 1: Код проекта менять можно! Одна из распространенных ошибок как в тестовых заданиях на собеседовании, так и при работе на проекте, что ничего нельзя менять. Конечно при правках в рабочем проекте обязательно нужно проконсультироваться/проревьюироваться у авторов кода (находится по истории VCS) +- 2: Наследовать `UserMealWithExceed` от `UserMeal` я не буду, т.к. это разные сущности: Transfer Object и Entity. Мы будет их проходить на 2м уроке. +- 3: Правильная реализация должна быть простой и красивой, можно сделать 2-мя способами: через стримы и через циклы. Сложность должна быть O(N), т.е. без вложенных стримов и циклов. +- 4: При реализации через циклы посмотрите в `Map` на методы `getOrDefault` или `merge` +- 5: **При реализации через `Stream` заменяйте `forEach` оператором `stream.map(..)`** +- 6: Объявляйте переменные непосредственно перед использованием (если возможно - сразу с инициализацией). При объявлении коллекций используйте тип переменной - интерфейс (Map, List, ..) +- 7: Если IDEA предлагает оптимизацию (желтым подчеркивает), например заменить лямбду на метод-референс, соглашайтесь (Alt+Enter) +- 8: Пользуйтесь форматированием кода в IDEA: `Alt+Ctrl+L` +- 9: Перед check-in проверяйте чендж-лист (курсор на файл и Ctrl+D): не оставляйте в коде ничего лишнего (закомментированный код, TODO и пр.). Если файл не меняется (например только пробелы или переводы строк), не надо его чекинить, делайте ему `revert` (Git -> Revert / `Ctrl+Alt+Z`). +- 10: `System.out.println` нельзя делать нигде, кроме как в `main`. Позже введем логирование. +- 11: Результаты, возвращаемые `UserMealsUtil.getFilteredWithExceeded` мы будем использовать [в нашем приложении](http://topjava.herokuapp.com/) для фильтрации по времени и отображения еды правильным цветом. +- 12: Обращайте внимание на комментарии к вашим коммитам в git. Они должны быть короткие и информативные (лучше на english) +- 13: Не полагайтесь в решении на то, что список будет подаваться отсортированным. Такого условия нет. +----- + +### Полезные ресурсы +> ВНИМАНИЕ: +> - ДЗ первого урока будет связано с созданием небольшого CRUD приложения (в памяти, без DB) на JSP и сервлетах. Введение будет, но предварительное знакомство не помешает. +> - основы JavaSсript необходимы для понимания проекта, начиная с 8-го занятия! + +Все остальное - опционально. + +#### HTML, JavaScript, CSS +- [Basic HTML and HTML5](https://learn.freecodecamp.org/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/) +- [Справочник по WEB](https://developer.mozilla.org/ru/) +- [Видео по WEB технологиям](https://www.youtube.com/user/WebMagistersRu/playlists) +- [Изучение JavaScript в одном видео уроке за час](https://www.youtube.com/watch?v=QBWWplFkdzw) +- HTML, CSS, JAVASCRIPT, SQL, JQUERY, BOOTSTRAP +- Введение в программирование на JavaScript +- Стандарты кодирования для HTML, CSS и JavaScript’a +- Основы работы с HTML/CSS/JavaScript +- JavaScript - Основы +- Основы JavaScript +- Bootstrap 3 - Основы +- jQuery для начинающих + +#### Java (базовые вещи) +- Интуит. Программирование на Java +- 1й урок MasterJava: Многопоточность +- Основы Java garbage collection +- Размер Java объектов +- Введение в Java Reflection API +- Структуры данных в картинках +- Обзор java.util.concurrent.* +- Синхронизация потоков +- String literal pool +- Маленькие хитрости Java +- A Guide to Java 8 + +### Туториалы, разное +[Что нужно знать о бэкенде новичку в веб-разработке](https://tproger.ru/translations/backend-web-development) +[Туториалы: Spring Framework, Hibernate, Java Core, JDBC](http://proselyte.net/tutorials/) + +#### Сервлеты +- Как создать Servlet? Полное руководство. + +#### JDBC, SQL +- Основы SQL на примере задачи +- Уроки по JDBC +- Learn SQL +- Интуит. Основы SQL +- Try SQL +- Курс "Введение в базы данных" + +#### Разное +- Вопросы по собеседованию, ресурсы для подготовки +- Эффективная работа с кодом в IntelliJ IDEA +- Quizful- тесты онлайн +- Введение в Linux + +#### Книги +- Джошуа Блох: Java. Эффективное программирование. Второе издание +- Гамма, Хелм, Джонсон: Приемы объектно-ориентированного проектирования. Паттерны проектирования +- Редмонд Э.: Семь баз данных за семь недель. Введение в современные базы данных и идеологию NoSQL +- Brian Goetz: Java Concurrency in Practice +- G.L. McDowell: Cracking the Coding Interview diff --git a/ReleaseNotes.md b/ReleaseNotes.md new file mode 100644 index 000000000000..5298ae94db76 --- /dev/null +++ b/ReleaseNotes.md @@ -0,0 +1,179 @@ +# TopJava Release Notes +### Topjava 15 +- Миграция на Servlet API 4.0 / Tomcat 9.x +- [Миграция на JDK11](http://javaops.ru/view/resources/jdk8_11) +- JUnit5 fix: junit-platform-surefire-provider не нужен +- Рефакторинг тестов: + - в `RootControllerTest.testUsers` для проверки используем `AssertionMatcher` адаптер + - вместо `content().json()` от `jsonassert` десериализуем json и используем сравнения через `AssertJ` +- В javascript место глабальных переменных и одинаковой функции обновления таблицы задаю их в объекте контекст, который передаю в `makeEditable()` как параметр +- Починил `back` в браузере после логина. Кнопки входа и регистрации отображаю только для `isAnonymous()` + +### Topjava 14 +- [Миграция на JUnit 5](http://javaops.ru/view/resources/junit5) +- Для измерения времени в тестах использую [Spring StopWatch](https://www.logicbig.com/how-to/code-snippets/jcode-spring-framework-stopwatch.html) +- `SimpleJdbcInsert` и `NamedParameterJdbcTemplate` конструируются (и берут настройки) из `jdbcTemplate` +- `AuthorizedUser` зарефакторился в `SecurityUtil` +- В javascript [заменил `var` на `let/const`](https://learn.javascript.ru/let-const). [Поддержка 95% браузеров](https://caniuse.com/#feat=const) +- Подправил UI фильтрации и заголовка страниц, добавилась кнопка `Cancel` в профиль +- Починил [баг в FireFox](https://bugzilla.mozilla.org/show_bug.cgi?id=884693): пустой ответ по ajax +- Сделал вход в приложение при нажании кнопок `Зайти как ...` +- Добавил регистрацию пользователя по REST +- Преименовал js файлы согласно [javascript filename naming convention](https://stackoverflow.com/questions/7273316/what-is-the-javascript-filename-naming-convention) +- Сделал проверку startTime/endTime на фильтре времени (после обновления datetimepicker до 2.5.20) + +### Topjava 13 +- [Миграция на Botstrap 4](https://getbootstrap.com/docs/4.1/migration/) +- Добавил [Responsive behaviors](https://getbootstrap.com/docs/4.1/components/navbar/#responsive-behaviors) - при уменшении ширины экрана навигация сворачивается в кнопку +- Для отображения цвета еды и выключенного юзера использую [data-* атрибуты](https://developer.mozilla.org/ru/docs/Web/Guide/HTML/Using_data_attributes) +- В `inputField.tag` передаю как параметр код для локализации label, а в `i18n.jsp` передаю как параметр `page`. См. [JSP include action with parameter example](https://beginnersbook.com/2013/12/jsp-include-with-parameter-example) + +### Topjava 12 +- [Миграция на Spring 5](http://javaops.ru/view/resources/spring5) +- обновил версии: Ehcache 3.x, datatables, datetimepicker +- добавил видео решений HW0 с одним проходом +- поправил видео [Обзор Spring Framework. Spring Context](https://drive.google.com/file/d/1fBSLGEbc7YXBbmr_EwEHltCWNW_pUmIH). Дописал про Constructor injection. +- заменил видео про тетсирование сервисов. Вместо самодельных матчеров стали использовать [AssertJ](http://joel-costigliola.github.io/assertj/index.html). Видео [Тестирование UserService через AssertJ](https://drive.google.com/open?id=1SPMkWMYPvpk9i0TA7ioa-9Sn1EGBtClD), время 1:53 +- сделал [видео с jQuery конвертерами и дефолтными группами валидации при сохранении в базу](https://drive.google.com/open?id=1tOMOdmaP5OQ7iynwC77bdXSs-13Ommax) +- сделал [видео с новым `DelegatingPasswordEncoder` и Json READ/WRITE access](https://drive.google.com/file/d/1XZXvOThinzPw4EhigAUdo8-MWT_g8wOt/view?usp=sharing) +- убрал `AccessType.PROPERTY` для `AbstractBaseEntity.id` (см. [fixed HHH-3718](https://hibernate.atlassian.net/browse/HHH-3718)) +- удалил `PasswordUtil`, возвращаю статус `NO_CONTENT` для REST delete, убрал группы валидации в `UserTo` +- заменил в jQuery [success на done](https://stackoverflow.com/a/22213543/548473) +- вместо `lang.jsp` сделал общий `bodyHeader.jsp` + +### Topjava 11 +- добавил + - доп. решение HW1 через одним return и O(N) + - раскрасил лог ([Logback layouts coloring](https://logback.qos.ch/manual/layouts.html#coloring)) +- рефакторинг + - починил коммит формы по cancel (`history.back()`) в FireFox + - заменил неработающий DependencyCi на [VersionEye](https://www.versioneye.com/) c проверкой зависимостей на uptodate + - починил `CrudUserRepository.getWithMeals()` через `@EntityGraph`. С неколькими ролями (у админа) еда дублируется + - починил тесты контроллеров с профилем JDBC (`JpaUtil` отсутствует в контексте JDBC) + - переименовал `meal.jsp/user.jsp` в `mealForm.jsp/userForm.jsp` + - в `InMemoryMealRepositoryImpl.save()` сделал update атомарным + - переименовал методы сервисов `save` в `create` + - переименовал и cделал классы `BaseEntity` и `NamedEntity` абстрактными + - обновил Noty и API с ним до 3.1.0. Добавил glyphicon в сообщения Noty + - заменил `MATCHER_WITH_EXCEED` на валидацию через [JSONassert](https://github.com/skyscreamer/JSONassert). + - поменял Deprecated валидаторы `org.hibernate.validator.constraints` на `javax.validation.constraints` + - убрал пароль из результатов REST через [@JsonProperty READ_ONLY / WRITE_ONLY](https://stackoverflow.com/questions/12505141/only-using-jsonignore-during-serialization-but-not-deserialization/12505165#12505165). Тесты на REST пришлось починить добавлением добавлением в JSON пароля как дополнительного параметра (`JsonUtil.writeWithExtraProps`) + - **убрал JSON View и сделал преобразование времени на UI с помощью [jQuery converters](http://api.jquery.com/jQuery.ajax/#using-converters)** + - **поменял [группу валидации по умолчанию при сохранении через JPA](https://stackoverflow.com/questions/16930623/16930663#16930663).** Теперь + все валидаторы в модели работаю по умолчанию (`groups` не требуется). + - Добавил в `ErrorInfo` тип ошибки `ErrorType` + i18n. + +- правки + - переименовал `ModelMatcher` в `BeanMatcher` и починил: можно сравнивать только упорядоченные коллекции (List) + - поменял зависимость `org.hibernate:hibernate-validator` на `org.hibernate.validator:hibernate-validator` (warning при сборке) + +### Topjava 10 +- добавил + - доступ к AuthorizedUser через [`@AuthenticationPrincipal`](http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-authentication-principal) и [authentication Tag](http://docs.spring.io/spring-security/site/docs/current/reference/html/taglibs.html#the-authentication-tag) + - [Обработку 404 NotFound](https://stackoverflow.com/questions/18322279/spring-mvc-spring-security-and-error-handling) + - локализацию ошибок валидации + - проверки json в тестах через [JSONassert](https://github.com/skyscreamer/JSONassert) и [через jsonPath](https://www.petrikainulainen.net/programming/spring-framework/integration-testing-of-spring-mvc-applications-write-clean-assertions-with-jsonpath/) + - [логирование от Postgres Driver](http://stackoverflow.com/a/43242620/548473) + - в `.travis.yml` [сборку только ветки master](https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches) + - [защиту от кэширование ajax запросов в IE](https://stackoverflow.com/a/4303862/548473) + - обработку запрета модификации системный юзеров через универсальный `ApplicationException` + - рефакторинг + - сделал `@EntityGraph` через `attributePaths` + - реализаовал обработку дублирования `user.email` и `meal.dateTime` через [Controller Based Exception Handling](https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc#controller-based-exception-handling) + - поменял отключение транзакционности в тестах через `@Transactional(propagation = Propagation.NEVER)` + - сделал выбор в сервлете через switch + - [все логгирование сделал через {} форматирование](http://stackoverflow.com/questions/10555409/logger-slf4j-advantages-of-formatting-with-instead-of-string-concatenation) и поправил его в контроллерах (поле проверки id) + - [перешел на конструктор DI](http://stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it) + - в `ModelMatcher` переименовал `Comparator` -> `Equality` + - [заинлайнил все лямбды](http://stackoverflow.com/questions/19718353/is-repeatedly-instantiating-an-anonymous-class-wasteful) (компараторы, ModelMatcher.equality) + - поменялась реализация `JdbcUserRepositoryImpl.getAll()` + - на UI кнопки в таблице заменились на линки, поправил сообщения локализации + - [сделал кастомизацию JSON (@JsonView) и валидацию (groups)](https://drive.google.com/file/d/0B9Ye2auQ_NsFRTFsTjVHR2dXczA) для данных еды, отдаваемых на UI + - в `JdbcUserRepositoryImpl` поменял `MapSqlParameterSource` на `BeanPropertySqlParameterSource` +- удалил + - зависимость `javax.transaction.jta` (уже не нужна) + - `${spring.version}` в `pom.xml` зависимостях (уже есть в `spring-framework-bom`) + - distinct из запроса Hibernate на пользователей с ролями. [Оптимизация запроса distinct: 15.16.2](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct) + - лишние `
` тэги (`shadow` и `view-box`) + +### Topjava 9 +- добавил + - выбор профиля базы через `ActiveProfilesResolver`/`AllActiveProfileResolver` на основе драйвера базы в classpath + - видео Cascade. Auto generate DDL. + - проверку на правильность id в Ajax/Rest контроллерах (treat IDs in REST body) + - тесты на валидацию входных значений контроллеров и зависимость на имплементацию + - Bootstrap Glyphicons +- рефакторинг + - переименовал `TimeUtil` в `DateTimeUtil` + - переименовал `ExceptionUtil` в `ValidationUtil` + - заменил валидацию `@NotEmpty` на `@NotBlank` + - заменил `CascadeType.REMOVE` на `@OnDelete` + - изменил `JdbcUserRepositoryImpl.getAll()` + - обновил jQuery до 3.x, исключил из зависимостей webjars ненужные jQuery + - cделал загрузку скриптов асинхронной + - фильтр еды сделал в [Bootstrap Panels](http://getbootstrap.com/components/#panels) + - вместо `Persistable` ввел интерфейс `HasId` и наследую от него как Entity, так и TO + - сделал универсальную обработку исключений дублирования email и dateTime + +### Topjava 8 +- добавил: + - [защиту от XSS (Cross-Site Scripting)](http://stackoverflow.com/a/40644276/548473) + - интеграцию с Dependency Ci и Travis Ci + - локализацию календаря + - сводку по результатам тестов + - примеры запросов curl в `config/curl.md` + - DataTables/Bootstrap 3 integration + - тесты на профиль деплоя Heroku (общее количество JUnit тестов стало 102) +- удалил зависимость `jul-to-slf4j` +- рефакторинг + - переименовал все классы `UserMeal**` в `Meal**`, JSP + - переименовал `LoggedUser` в `AuthorizedUser` + - починил работа с PK Hibernate в случае ленивой загрузки (баг HHH-3718) + - поменял в `BaseEntity` `equals/hashCode/implements Persistable` + - в `InMemoryMealRepositoryImpl` выделил метод `getAllStream` + - перенес проверки пердусловий `Assert` из `InMemory` репозиториев в сервисы + - переименовал классы _Proxy*_ на более адекватные _Crud*_ + - поменял реализацию `JpaMealRepositoryImpl.get`, добавил в JPA модель `@BatchSize` + - вместо `@RequestMapping` ввел Spring 4.3 аннотации `@Get/Post/...Mapping` + - поменял авторизацию в тестах не-REST контроллеров + - перенес вызовы `UserUtil.prepareToSave` из `AbstractUserController` в `UserServiceImpl` + - зарефакторил обработку ошибок (`ExceptionInfoHandler`) + +### Topjava 7 +- добавил: + - [JPA 2.1 EntityGraph](https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs002.htm) + - [Jackson @JsonView](https://habrahabr.ru/post/307392/) + - валидацию объектов REST + - [i18n в JavaScript](http://stackoverflow.com/a/6242840/548473) + - проверку предусловий и видео Методы улучшения качества кода + - интеграцию с проверкой кода в Codacy + - [сравнение вермени исполнения запросов при различных meals индексах](https://drive.google.com/open?id=0B9Ye2auQ_NsFX3RLcnJCWmQ2Y0U) +- tomcat7-maven-plugin плагин перключили на Tomcat 8 (cargo-maven2-plugin) +- рефакторинг + - обработка ошибок сделал с array + - матчеров тестирования (сделал автоматические обертки и сравнение на основе передаваемого компаратора) + - вынес форматирование даты в `functions.tld` + +### Topjava 3-6 +- добавил + - [выпускной проект](https://drive.google.com/open?id=0B9Ye2auQ_NsFcG83dEVDVTVMamc) + - в таблицу meals составной индекс + - константы `Profiles.ACTIVE_DB`, `Profiles.DB_IMPLEMENTATION` + - проверки и тесты на `NotFound` для `UserMealService.getWithUser` и `UserService.getWithMeals` + - в MockMvc фильтр CharacterEncodingFilter + - защиту от межсайтовой подделки запроса, видео Межсайтовая подделка запроса (CSRF) + - ограничение на диапазон дат для фильтра еды +- рефакторинг + - UserMealsUtil, ProfileRestController, компараторов в репозитоии + - `LoggedUser` отнаследовал от `org.springframework.security.core.userdetails.User` + - переименовал `DbTest` в `AbstractServiceTest` и перенес сюда `@ActiveProfiles` + - сделал выполнение скриптов в тестах через аннотацию `@Sql` + - вместо использования id и селектора сделал обработчик `onclick` + - изменил формат ввода даты в форме без 'T' +- убрал + - `LoggerWrapper` + - Dandelion обертку к datatables +- обновил + - Hibernate до 5.x и Hibernate Validator, добавились новые зависимости и `jackson-datatype-hibernate5` + - datatables API (1.10) + - Postgres драйвер. Новый драйвер поддерживает Java 8 Time API, разделил реализацию JdbcMealRepositoryImpl на Java8 (Postgresql) и Timestamp (HSQL) diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml new file mode 100644 index 000000000000..e1d3a10b37e4 --- /dev/null +++ b/config/Topjava-soapui-project.xml @@ -0,0 +1,590 @@ + + + + + + + + http://localhost:8080 + + + + + + + + + text/html;charset=utf-8 + 500 401 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/admin/users + + user@yandex.ru + password + No Authorization + Basic + No Authorization + + + + + + + + + + + application/json + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 201 + + user:Response + + + application/json;charset=UTF-8 + 201 + + user:Response + + + + <xml-fragment/> + + http://localhost:8080 + {"name": "New2", + "email": "new2@yandex.ru", + "password": "passwordNew", + "roles": ["ROLE_USER"] +} + + http://localhost/topjava/rest/admin/users + + No Authorization + Basic + No Authorization + + + + + + + + + + + + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + ns:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/admin/users/100000 + + No Authorization + Basic + No Authorization + + + + + + + + + + + text/html;charset=utf-8 + 405 500 + + html + + + application/json + + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + <xml-fragment/> + + + http://localhost:8080 + {"name": "UserUpdated", + "email": "user@yandex.ru", + "password": "passwordNew", + "roles": ["ROLE_USER"] +} + + http://localhost/topjava/rest/admin/users/100000 + + Basic + Basic + Global HTTP Settings + + + + + + + + + + + + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + prof:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile + + Basic + Basic + Global HTTP Settings + + + + + + + + + + + application/json + + + + text/html;charset=utf-8 + 500 405 + + html + + + application/json + 201 + + user:Response + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + 200data + + <xml-fragment/> + + http://localhost:8080 + {"name": "New777", + "email": "new777@yandex.ru", + "password": "passwordNew", + "roles": ["ROLE_USER"] +} + + http://localhost/topjava/rest/profile + + No Authorization + Basic + No Authorization + + + + + + + + + + + + 200 + + data + + + text/html;charset=utf-8 + 500 + + html + + + application/json + + + + + 200 + + data + + 204data + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile + + No Authorization + Basic + No Authorization + + + + + + + + + + + + + + + + 0 + + data + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals + + No Authorization + + + + + + + + + + + application/json + + + + application/json;charset=UTF-8 + 201 + + meal:Response + + + + <xml-fragment/> + + + http://localhost:8080 + { +"dateTime": "2015-06-01T10:00", +"description": "Новый завтрак", +"calories": 777 +} + + http://localhost/topjava/rest/profile/meals + + No Authorization + + + + + + + + + + + + + + + application/json;charset=UTF-8 + 200 + + ns:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals/100002 + + No Authorization + + + + + + + + + + + + + 200 + + data + + + application/json + + + + + 200 + + data + + 200data + + <xml-fragment/> + + UTF-8 + http://localhost:8080 + { +"id": 100002, +"dateTime": "2015-05-30T10:00", +"description": "Обновленный завтрак", +"calories": 500 +} + + http://localhost/topjava/rest/profile/meals/100002 + + No Authorization + + + + + + + + + + + + + startDate + + QUERY + + + + + startTime + + QUERY + + + + + endDate + + QUERY + + + + + endTime + + QUERY + + + + + + + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals/filter + + No Authorization + + + + + + + + + + + startDate + startTime + endDate + endTime + + + + + + + + + + + \ No newline at end of file diff --git a/config/curl.md b/config/curl.md new file mode 100644 index 000000000000..68fb70bf9de9 --- /dev/null +++ b/config/curl.md @@ -0,0 +1,33 @@ +### curl samples (application deployed in application context `topjava`). +> For windows use `Git Bash` + +#### get All Users +`curl -s http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` + +#### get Users 100001 +`curl -s http://localhost:8080/topjava/rest/admin/users/100001 --user admin@gmail.com:admin` + +#### get All Meals +`curl -s http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` + +#### get Meals 100003 +`curl -s http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` + +#### filter Meals +`curl -s "http://localhost:8080/topjava/rest/profile/meals/filter?startDate=2015-05-30&startTime=07:00:00&endDate=2015-05-31&endTime=11:00:00" --user user@yandex.ru:password` + +#### get Meals not found +`curl -s -v http://localhost:8080/topjava/rest/profile/meals/100008 --user user@yandex.ru:password` + +#### delete Meals +`curl -s -X DELETE http://localhost:8080/topjava/rest/profile/meals/100002 --user user@yandex.ru:password` + +#### create Meals +`curl -s -X POST -d '{"dateTime":"2015-06-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` + +#### update Meals +`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` + +#### validate with Error +`curl -s -X POST -d '{}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` +`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00"}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` diff --git a/config/messages/app.properties b/config/messages/app.properties new file mode 100644 index 000000000000..e2afb7cbaabd --- /dev/null +++ b/config/messages/app.properties @@ -0,0 +1,65 @@ +app.title=Calories management +app.footer=Internship Spring 5/JPA Enterprise (Topjava) application +app.login=Login as +app.profile=profile +app.register=Registration +app.registered=You are registered. Please Sign in. + +user.title=Users +user.edit=Edit user +user.add=Add user +user.name=Name +user.email=Email +user.roles=Roles +user.active=Active +user.registered=Registered +user.password=Password +user.caloriesPerDay=Daily calorie limit + +userTo.name=Name +userTo.email=Email +userTo.password=Password +userTo.caloriesPerDay=Daily calorie limit + +meal.title=Meals +meal.edit=Edit meal +meal.add=Add meal +meal.filter=Filter +meal.startDate=From date +meal.endDate=To date +meal.startTime=From time +meal.endTime=To time +meal.description=Description +meal.dateTime=Date/Time +meal.calories=Calories + +common.add=Add +common.select=Select +common.deleted=Record deleted +common.saved=Record saved +common.enabled=Record enabled +common.disabled=Record disabled +common.confirm=Are you sure? +common.save=Save +common.cancel=Cancel +common.search=Search + +exception.common.notFound=Not found entity with {0} +exception.user.duplicateEmail=User with this email already exists +exception.user.modificationRestriction=Admin/User modification is forbidden +exception.meal.duplicateDateTime=You already have meal with this date/time + +error.appError=Application error +error.dataNotFound=Data not found +error.dataError=Data error +error.validationError=Validation error +error.wrongRequest=Wrong request + +NotEmpty="{0}" must not be empty +NotBlank="{0}" must not be empty +NotNull="{0}" must not be empty +Email= Invalid format of "{0}" +Range="{0}" must be between {2} and {1} +Length="{0}" length must be between {2} and {1} +Size="{0}" size must be between {2} and {1} +SafeHtml="{0}" may has unsafe html content \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties new file mode 100644 index 000000000000..6d2f71923a9e --- /dev/null +++ b/config/messages/app_ru.properties @@ -0,0 +1,67 @@ +app.title=Подсчет калорий +app.footer=Приложение по проекту Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery +app.login=Зайти как +app.profile=профиль +app.register=Регистрация +app.registered=Вы зарегистрированы. Введите ваш логин/пароль. + +user.title=Пользователи +user.edit=Редактирование пользователя +user.add=Добавление пользователя +user.name=Имя +user.email=Почта +user.roles=Роли +user.active=Активный +user.registered=Зарегистрирован +user.password=Пароль +user.caloriesPerDay=Норма калорий в день + +userTo.name=Имя +userTo.email=Почта +userTo.password=Пароль +userTo.caloriesPerDay=Норма калорий в день + +meal.title=Моя еда +meal.edit=Редактирование еды +meal.add=Добавление еды +meal.filter=Отфильтровать +meal.startDate=От даты +meal.endDate=До даты +meal.startTime=От времени +meal.endTime=До времени +meal.description=Описание +meal.dateTime=Дата/Время +meal.calories=Калории + +common.add=Добавить +common.select=Выбрать +common.deleted=Запись удалена +common.saved=Запись сохранена +common.enabled=Запись активирована +common.disabled=Запись деактивирована +common.errorStatus=Статус ошибки +common.appError=Ошибка приложения +common.confirm=Вы уверены? +common.save=Сохранить +common.cancel=Отменить +common.search=Искать + +exception.common.notFound=Не найдена запись с {0} +exception.user.duplicateEmail=Пользователь с такой почтой уже есть в приложении +exception.user.modificationRestriction=Модификация Admin/User запрещена +exception.meal.duplicateDateTime=У вас уже есть еда с такой датой/временем + +error.appError=Ошибка приложения +error.dataNotFound=Данные не найдены +error.dataError=Ошибка в данных +error.validationError=Ошибка проверки данных +error.wrongRequest=Неверный запрос + +NotEmpty=Поле "{0}" не должно быть пустым +NotBlank=Поле "{0}" не должно быть пустым +NotNull=Поле "{0}" не должно быть пустым +Email=Неверный формат поля "{0}" +Range=Поле "{0}" должно быть между {2} и {1} +Length=Длинна поля "{0}" должена быть между {2} и {1} +Size=Размер поля "{0}" должен быть между {2} и {1} +SafeHtml=Поле "{0}" может содержать небезопасный html контент \ No newline at end of file diff --git a/cv.md b/cv.md new file mode 100644 index 000000000000..2571ae7a0605 --- /dev/null +++ b/cv.md @@ -0,0 +1,109 @@ +## Составление резюме, подготовка к интервью, поиск работы + +![cv](https://cloud.githubusercontent.com/assets/13649199/10877471/93ea86b8-8157-11e5-9bfa-95e3fba75c58.jpg) + +- Научиться программировать сложнее, чем кажется +- [Собеседование. Разработка ПО. Вопросы.](https://drive.google.com/open?id=0B9Ye2auQ_NsFQVc2WUdCR0xvLWM) +- [Набор ссылок для тренировки и прохождения интервью](https://github.com/andreis/interview) + +### Составление резюме: +- [VisualCV: create resume in minutes](https://www.visualcv.com/) +- Выбрать шаблон для резюме +- [GitHub Pages](https://pages.github.com/), Resume template +- Как продать свое резюме в 2 раза дороже +- Как правильно составить резюме +- Резюме программистов. Часть 1 (плохие) +- Резюме программистов. Часть 2 (хорошие) +- Как составить резюме на английском +- ОФОРМЛЕНИЕ IT-РЕЗЮМЕ для USA + +### Наши истории (делимся опытом и успехом) + +### Тесты/задачи онлайн: +- [Java Programming Test](https://tests4geeks.com/java) +- game: test Java skills +- Codility lesson tests +- Quizful- тесты онлайн +- LeetCode Online Judge +- Sphere online judge +- Codility programmers lessons +- Hackerrank practice coding + +## [Тестовое собеседование, самые спрашиваемые темы](http://javaops.ru/interview/test.html) + +### Интервью: +- Михаил Портнов. Собеседование на работу: как продать себя грамотно +- Михаил Портнов. Какие вопросы мы задаем на собеседовании? +- Михаил Портнов. Собеседование на работу: жизненный путь +- Канал: Резюме, поиск работы, интервью +- Яков Файн: Как стать профессиональным Java разработчиком +- Ответы на вопросы на собеседовании Junior Java Developer +- Список вопросов с ответами для собеседования по Java +- Сборка по вопросам на интервью +- Сборка вопросов-ответов от JavaStudy +- [Вопросы по классам коллекциям от JavaRush-1](http://info.javarush.ru/translation/2013/10/08/Часто-задаваемые-на-собеседованиях-вопросы-по-классам-коллекциям-в-Java-Часть-1-.html) +- [Вопросы по классам коллекциям от JavaRush-2](http://info.javarush.ru/translation/2013/10/08/Часто-задаваемые-на-собеседованиях-вопросы-по-классам-коллекциям-в-Java-Часть-2-.html) +- Тест на знание SQL +- Вопросы на собеседовании Java Junior Developer +- Java вопросы с собеседований на Android +- Сборка вопросов от JavaRush +> про clone и finalize объязательно прочтите Джошуа Блох: Java. Эффективное программирование (второе издание) + +- Cracking the Coding Interview +> Особенно обратите внимание на раздел: Часть VIII. Вопросы собеседования + + +### От себя: +- email, skype - очень желательно, чтобы по ним вы были узнаваемы. Заведите рабочие, если не так. +- написать ВЕСЬ IT опыт (исключая опыт пользователя: Windows, MS Word, Photophop, Yandex disk, Google docs, ..): технологии, какие задачи решали (конкретные), какие инструменты использовали, VCS, DB, инструменты сборки, ... включая опыт в ВУЗе. +- на English иметь желательно. Если вакансия опублинована на Englsih - шлите на нем. Часто могут на нем попросить, если работодатель иностранный. +- удобно иметь резюме где то в инете (hh, linkedin, google doc, чтобы им было удобно делиться). + +### Позиционирование проекта Topjava: +- Обязательно убери из резюме **любое упоминание Junior**. Количество обращений возрастет на порядок. Ссылку на стажировку можно поставить: http://javaops.ru/view/topjava (в linkedin: https://www.linkedin.com/company/java-online-projects). +- После завершения проекта ты освошь все заявленные в нем технологии - вставь их в квалификацию (включая java 8 Stream and Time API). +- В разделе опыт работы (если нет коммерческого опыта) вставь: + + Участие в разработке Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей + на стеке Maven/ Spring MVC/ Security/ REST(Jackson)/ Java 8 Stream API: + - реализация сохранения в базы Postgres и HSQLDB на основе Spring JBDC, JPA(Hibernate) и Spring-Data-JPA + - реализация и тестирование REST и AJAX контроллеров + - реализация клиента на Bootstrap (css/js), datatables, jQuery + plugins. + - собственная доработка проекта + +- Делай упор не на обучение, а на **участие в проекте**. Выполнение домашних заданий это полноценное участие с написанием функционала по всем пройденным технологиям. На собеседовании смотрят не на то, что ты заканчивал, а на опыт и знания. + +### В процессе обучения +- Если рассмотриваешь предложения по работе, подними в своем профиле этот флаг и обязательно заполни ссылку на резюме. Обновления нашей базы выпускников смотрят уже более 125 партнеров по трудоустройству (компании и индивидуальные рекрутеры). Проверь содержание "Информация для HR": по нему принимают решение, открывать резюме или нет. + +- Вступайте в нашу группу участников Slack: каналы помощи с Java, отзывы о работодателях, обсуждение тестовых заданий, вакансии, цены на рынке труда, IT события, интересные видео и многое другое. + +- Подпишитесь на рассылку вакансий под себя + +### После прохождения испытательного срока жду твою [историю успеха](http://javaops.ru/view/story) + +### Основные сайты поиска работы: +- Яндекс агрегатор +- HH +- LinkedIn +- djinni.co (более актуально для Украины) + +## Как выжить на испытательном сроке +- Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В + процессе формулирования очень часто приходит ее решение. +- Учись инвестигировать проблему. Внимательное чтение логов и умение дебажить - основные навыки + разработчика. В логах надо читать верх самого нижнего эксепшена - там причина всей портянки. +- Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и + бится нам ней часами. + Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время. +- Если тебе что-то объясняют по проекту - обязательно записывай. +- Когда получаешь задачу - уточни все очень подробно. +- Получай в процессе решения обратную связь - в том ли направлении ты идешь. +- Не игнорируй совместные ланчи (курилки) +- Готовься к стендапам/летучкам. Задавай на них вменяемые вопросы. Выказывай заинтересованность +- Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи. +- [**5 вещей, которые разработчик должен сделать прежде чем попросить о помощи**](https://techrocks.ru/2018/07/16/5-things-a-developer-should-do-before-asking-for-help/) +- [**Советы новичкам**](http://blog.csssr.ru/2016/09/19/how-to-be-a-beginner-developer) +- [Нетехнические навыки](https://tproger.ru/experts/softskills-for-job) + +## [Отзывы по стажировке Topjava](https://vk.com/topic-74381644_30447246) diff --git a/description.md b/description.md new file mode 100644 index 000000000000..d2448ca99e57 --- /dev/null +++ b/description.md @@ -0,0 +1,77 @@ +#### Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), datatables, jQuery + plugins, Java 8 Stream and Time API и сохранением в базах данных Postgresql и HSQLDB. + +- Основное внимание будет уделяться способам решения многочисленных проблем разработки в Spring/JPA, а также структурному (красивому и надежному) java кодированию и архитектуре приложения. +- Каждая итерация проекта закрепляется домашним заданием по реализации схожей функциональности. Следующее занятие начинается с разбора домашних заданий. +- Большое внимание уделяется тестированию кода: в проекте более 100 JUnit тестов. +- Несмотря на относительно небольшой размер, приложение разрабатывается с нуля как большой проект (например мы используем кэш 2-го уровня Hibernate, настраиваем Jackson для работы с ленивой загрузкой +Hibernate, делаем конверторы для типов LocalDateTime (Java 8 time API). + Разбираются архитектурные паттерны: слои приложения и как правильно разбивать логику по слоям, когда нужно применять Data Transfer Object. + Т.е на выходе получается не учебный проект, а хорошо маштабируемый шаблон для большого проекта на всех пройденных технологиях. +- Большое внимание уделяется деталям: популяция базы, использование транзакционности, тесты сервисов и REST + контроллеров, насторойка EntityManagerFactory, + выбор реализации пула коннектов. Особое внимание уделяется работе с базой: через Spring JDBC, Spring ORM и + Spring Data Jpa. +- Используются самые востребованные на сегодняшний момент фреймворки: Maven, Spring Security 4 + вместе с Spring Security Test, наиболее удобный для работы с базой проект Spring Data Jpa, библиотека логирования logback, реализующая SLF4J, повсеместно используемый Bootstrap и jQuery. + +#### Демо разрабатываемого приложения + +## План проекта (ссылки на некоторые темы открыты для просмотра) +### Архитектура проекта. Персистентность. +- Системы управления версиями +- Java 8: Lambda, Stream API +- Обзор используемых в проекте технологий и инструментов. +- Инструмент сборки Maven. +- WAR. Веб-контейнер Tomcat. Сервлеты. +- Логирование. +- Обзор стандартных библиотек. Apache Commons, Guava +- Слои приложения. Создание каркаса приложения. +- Обзор Spring Framework. Spring Context. +- Тестирование через JUnit. +- Spring Test +- Базы данных. PostgreSQL. Обзор NoSQL и Java persistence solution без ORM. +- Настройка Database в IDEA. +- Скрипты инициализации базы. Spring Jdbc Template. +- Spring: инициализация и популирование DB +- ORM. Hibernate. JPA. +- [Тестирование JPA сервиса через AssertJ](https://www.youtube.com/watch?v=BlyaXT6tOaw) +- Поддержка HSQLDB +- Транзакции +- Профили Maven и Spring +- Пул коннектов +- Spring Data JPA +- Кэш Hibernate + +### Разработка WEB +- Spring кэш +- Spring Web +- JSP, JSTL, i18n +- Tomcat maven plugin. JNDI +- Spring Web MVC +- Spring Internationalization +- Тестирование Spring MVC +- REST контроллеры +- Тестирование REST контроллеров. Jackson. +- jackson-datatype-hibernate. Тестирование через матчеры. +- Тестирование через SoapUi. UTF-8 +- WebJars. +- Bootstrap. jQuery datatables. +- AJAX. jQuery. Notifications. +- Spring Security +- Spring Binding/Validation +- Работа с datatables через Ajax. +- Spring Security Test +- [Кастомизация JSON (@JsonView) и валидации (groups)](https://drive.google.com/open?id=0B9Ye2auQ_NsFRTFsTjVHR2dXczA) +- Encoding password +- CSRF (добавление в проект защиты от межсайтовой подделки запроса) +- form-login. Spring Security Taglib +- Handler interceptor +- Spring Exception Handling +- Смена локали +- Фильтрация JSON через @JsonView +- Защита от XSS (Cross Site Scripting) +- Деплой в Heroku +- Локализация datatables, ошибок валидации +- Обработка ошибок 404 (NotFound) +- Доступ к AuthorizedUser +- Собеседование. Разработка ПО diff --git a/doc/lesson01.md b/doc/lesson01.md new file mode 100755 index 000000000000..e0ff979895f7 --- /dev/null +++ b/doc/lesson01.md @@ -0,0 +1,287 @@ +# Онлайн проект Topjava +- Не стоит стремиться прочитать все ссылки урока, их можно использовать как справочник. Гораздо важнее пройти основной материал урока и сделать Домашнее Задание +- Обязательно посмотри правила работы с патчами на проекте +- Делать Apply Patch лучше по одному, непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (`Version Control->Local Changes-> Ctrl+D`) +- При первом Apply удобнее выбрать имя локального ченджлиста Name: Default. Далее все остальные патчи также будут в него попадать. +- **Код проекта обновляется и не всегда совпадает с видео (можно увидеть как развивался проект). Изменения в проекте указываю после соответствующего патча.** + +## Материалы занятия (скачать все патчи можно через `Download/Скачать` папки patch) +![image](https://cloud.githubusercontent.com/assets/13649199/18330295/5f2ca214-7560-11e6-8e1e-c0494f798c37.png) + +### ![correction](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Рефакторинг проекта + +#### Apply 1_0_fix.patch +- переименовал классы `UserMeal*` в более красивые `Meal*` +- поправил грамматику: `exceed` (глагол) на `excess` +- преименовал `MealWithExceed` transfer object класс (что это такое пройдем позже) в `MealTo` ([data transfer object naming convention](https://stackoverflow.com/questions/1724774/java-data-transfer-object-naming-convention)) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW0: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Optional: реализация getFilteredMealsWithExcess через Stream API +#### Apply 1_1_HW0_stream.patch + +- [Презентация Java 8](https://docs.google.com/presentation/d/1oltLkHK60FqIdsXjUdm4pPLSeC6KpNYjDsM0ips-qBw) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Работа с git в IDEA. Реализация через цикл. +#### Apply 1_2_HW0_cycle.patch + +> Изменения в проекте: `map.getOrDefault` заменил на `map.merge`, `for` заменены на `forEach` + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW0 + +> что делает метод `Map.merge` ? + +никогда не надо лениться зайти в код `Map.merge` (`Ctrl+мышка` на методе) и почитать javadoc (`Ctrl+Q`). Когда привыкать, если не сейчас? + +> что означает `Integer::sum` ? + +это ссылка на метод, сокращенная форма лямбды. IDEA иногда выделяет желтым и предлагает замену (соглашаться по `Alt+Enter`). +Например `m->m.getCalories()` заменяется на `Meal::getCalories`. + +> почему не использовать в `TimeUtil` методы `isBefore/isAfter` ? + +это строгие (excluded) сравнения, а нам также нужны краевые значения + +> В `MealsUtil` у нас где-то есть ключевое слово `final`, где-то нет. В чем разница? + +Я участвовал в одном проекте, где `final` был обязательным (в сеттингах IDEA галочка стояла). Но это скорее исключение, чем правило в проектах java (в Java 8 вообще ввели эффективный final, те по факту). Во всех новомодных языках переменные final по умолчанию, а в java нужно помнить и везде добавлять, утомительно. Но если приучитесь - хуже не будет. Я обычно ставлю там, где важно по смыслу (если не забываю). + +> Почему у `User.registered` тип `Date`, а `Meal.dateTime` `LocalDateTime `? + +Время регистрации - абсолютное (конкретный момент), а время еды по бизнес логике относительно (те не зависит от часового пояса) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. HW0 Optional 2: реализация в один проход и одной строкой Stream API +#### Apply 1_3_HW0_optional2.patch + +## Занятие 1: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Обзор используемых в проекте технологий. Интеграция ПО. +- [**RebelLabs Developer Productivity Report 2017: Why do you use the Java tools you use**](https://zeroturnaround.com/rebellabs/developer-productivity-report-2017-why-do-you-use-java-tools-you-use/) +- Обзор популярности инструментов и технологий Java за 2016 г. +- Обзор популярности инструментов и технологий Java за 2014 г. +- Видео "Приложение Spring Pet Clinic" +- Приложение Spring Pet Clinic. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Maven. +- Среда сборки проектов Maven. +- The Central Repository +- The Reactor. Snapshots +- Недостатки Maven. Другие инструменты сборки. +- Справочник: + - Видео: Maven vs Gradle vs SBT (Архипов, Борисов, Садогурский) + - Автоматизация сборки проекта + - Home Page + - Maven: The Complete Reference + - Разработка ПО вместе с maven + - Build Lifecycle + - Dependency Mechanism + - Создание своих архетипов и каталогов в Maven + - Зависимости, профили + - Bintray: gateway to Maven Central + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. WAR. Веб-контейнер Tomcat. Сервлеты. +> - Обновил зависимость до Servlet 4.0. Установите себе [Tomcat 9.x](https://tomcat.apache.org/download-90.cgi) +> - Устанавливать Tomcat лучше простым скачиванием архива `xxx.zip` и копированием из него в **каталог без пробелов и русских букв** (пример `С:\java\apache-tomcat-9.0.14`) + +#### Apply 1_4_switch_to_war.patch +> - Обновил сервлеты до версии 4.0 (Tomcat 9 использует это API, хотя для нас не принципиально, т.к. мы никакие фичи 3.x и 4.x Servlet API не используем) +> - Переименовал `userList.jsp` в `users.jsp` +> - Сервлет добавляется в следующем патче, те **в `web.xml` он будет подчеркиваться красным.** + +#### Apply 1_5_add_servlet_api.patch + +> - Если зависимость `servlet-api` не подтянулась, сделай `Reimport All Maven Projects` (см. [Обновить зависимости в maven проекте](https://github.com/JavaOPs/topjava/wiki/IDEA#Обновить-зависимости-в-maven-проекте)). +**Все зависимости в Maven прект подтягиваются ТОЛЬКО через Maven**. +> - [Проверка, кто занял порт](https://stackoverflow.com/a/38953356/548473) (в случае проблем с запуском и дебагом на портах 8080, 8000) +> - [**Деплой war в Tomcat с Application context**](https://github.com/JavaOPs/topjava/wiki/IDEA#Деплой-war-в-tomcat-application-context-должен-быть-тот-же-что-и-url-приложения-деплоить-надо-war-exploded) +> - [**Динамическое обновление без передеплоя**](https://github.com/JavaOPs/topjava/wiki/IDEA#Для-динамической-перегрузки-ресурсов-кнопка-нажмите-кнопку-update-resource-on-frame-deactivation) + +#### Apply 1_6_forward_to_redirect.patch + +- Tomcat Home Page +- Сервлеты +- Руководство: как создать servlet +- Томкат менеджер: [http://localhost:8080/manager](http://localhost:8080/manager) + - в `TOMCAT_HOME\conf\tomcat-users.xml` нужно добавить +``` + +``` +- Наше приложение: [http://localhost:8080/topjava](http://localhost:8080/topjava) +- Наш сервлет: [http://localhost:8080/topjava/users](http://localhost:8080/topjava/users) +- Справочник: + - [HTTP](https://developer.mozilla.org/ru/docs/Web/HTTP) + - Отладчик IntelliJ IDEA + - Yakov Fain: Intro to Java EE. Glassfish. Servlets (по-русски) + - Yakov Fain: HTTP Sessions, Cookies, WAR deployments, JSP (по-русски) + - Golovach Courses: Junior.February2014.Servlets + - Remotely debug on tomcat from IDEA + - Java Server Page + - Java объекты, доступные в JSP + - Java Server Pages (JSP) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Логирование. +#### Apply 1_7_logging.patch + +> **Установите переменную окружения на TOPJAVA_ROOT на корень проекта и перезапустите IDEA. Слеши в пути должны быть в стиле unix (/)** + +> - изменения в проекте: убрал `LoggerWrapper` и логирую напрямую в логгер SLF4J. При логгировании через вспомогательный класс, в логе теряется имя исходного класса. +> - удалил зависимость `jul-to-slf4j`. Она нам не нужна и, согласно видео Владимира Красильщика про логирование, она замедляет работу +> - удалил зависимость `jcl-over-slf4j`. Используем Spring 5, который напрямую использует `slf4j` без `common-logging`. Про миграцию на Spring 5 будет видео в конце стажировки. +> - Не делать конкатенацию строк при логгировании сообщений, если уровень логирования в конфигурации выставлен выше уровня логирования в коде +> - [slf4j formatting with {}](http://stackoverflow.com/a/10596390/548473) +> - [What is the fastest way of (not) logging](https://www.slf4j.org/faq.html#logging_performance) + +- Java Logging: история кошмара +- Ведение лога приложения +- Log4j, Logback +- Добавление зависимостей логирования в проект. +- Logback variable substitution + +#### Проверочные вопросы: +- Что нужно изменить в `pom.xml`, чтобы перейти с logback на log4j ? + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> Почему `private static final Logger log` а не `LOG` ? + +Это [правило именования констант, которые не "deeply immutable"](https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names), те если их содержимое можно изменить. + +> Используются ли еще где-то в реальной разработке JSP, или это уже устаревшая технология? Заменит ли ее JSF (https://javatalks.ru/topics/38037)? + +JSF и JSP- разные ниши и задачи. +JSP- шаблонизатор, JSF - МVС фреймворк. Из моего опыта- с JSP сталкивался в 60% проектов. Его прямая замена: http://www.thymeleaf.org (в Spring-Boot по умолчанию), но в уже запущенных проектах встречается достаточно редко. JSP не умирает, потому что просто и дешево. Кроме того включается в большинство веб-контейнеров (в Tomcat его реализация Jasper) + +JSF- sun-овский еще фреймворк, с которым я ни разу не сталкивался и особого желания нет. Вот он как раз, по моему мнению, активно замещается хотя бы javascript фреймворками (angular, ember, react). + +> А зачем мы использовали logback? Почему SLF4J нас не устроило? Почему реализация логирования не log4j? + +`SLF4J-API` это API. Там есть только пустая реализация `org.slf4j.helpers.NOPLogger` (можно посмотреть в исходниках). Logback для новых проектов стал стандарт. *spring-petclinic* и *spring-boot* используют его по умолчанию. +- http://logback.qos.ch/reasonsToSwitch.html + +--------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW01 (делаем ветку HW01 от последнего патча в master) + +> **ОСНОВНОЕ, чему мы учимся на проекте: мыслить и работать как Java разработчики уже сейчас**, потом это будет гораздо сложнее и стоить дороже. +Вот на мой взгляд [хорошие советы новичкам](http://blog.csssr.ru/2016/09/19/how-to-be-a-beginner-developer). От себя я добавлю: +> - Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В процессе формулирования очень часто приходит ее решение. +> - что я делаю (подробно, чтобы понял человек, который не копался в этом совсем) +> - что получаю (обычно верх самого последнего эксепшена) +> - мои попытки решения проблемы + +> - Учись исследовать проблему. Внимательное чтение логов и [умение дебажить](http://info.javarush.ru/idea_help/2014/01/22/Руководство-пользователя-IntelliJ-IDEA-Отладчик-.html) - основные навыки разработчика. Обычно самый верх самого нижнего эксепшена- причина ошибки, туда нужно ставить брекпойнт. +> - Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и биться над ней часами. Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время. + +#### 1. Реализовать сервлет с отображением в таблице списка еды (в памяти и БЕЗ учета пользователя) + +> Деплоиться в Tomcat лучше как `war exploded`: нет упаковки в war и при нажатой кнопке `Update Resources on Frame Deactivation` можно обновляться css, html, jsp без передеплоя. При изменении `web.xml`, добавлении методов, классов необходим redeploy. + +- 1.1 По аналогии с `UserServlet` добавить `MealServlet` и `meals.jsp` + - Задеплоить приложение (war) в Tomcat c `applicationContext=topjava` (приложение должно быть доступно по http://localhost:8080/topjava) + - Попробовать разные деплои в Tomcat, remote и local debug +- 1.2 Сделать отображения списка еды в JSP, цвет записи в таблице зависит от параметра `excess` (красный/зеленый). + - 1.2.1 Список еды захардкодить (те проинициализировать в коде, желательно чтобы в проекте инициализация была только в одном месте) + - 1.2.2 Время выводить без 'T' + - 1.2.3 Список выводим БЕЗ фильтрации по `startTime/endTime` + - 1.2.4 С обработкой исключений пока можно не заморачиваться, мы будем красиво обрабатывать в конце стажировки + - 1.2.5 Вариант реализации: + - из сервлета преобразуете еду в `List`; + - кладете список в запрос (`request.setAttribute`); + - делаете `forward` на jsp для отрисовки таблицы (при `redirect` атрибуты теряются). + - в `JSP` для цикла можно использовать `JSTL tag forEach`. Для подключения `JSTL` в `pom.xml` и шапку JSP нужно добавить: +``` + + javax.servlet + jstl + 1.2 + + + <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + ... +``` + + - Интернет-приложения на JAVA + - JSP + - [Как создать Servlet? Полное руководство](https://devcolibri.com/как-создать-servlet-полное-руководство) + - [JSTL для написания JSP страниц](https://devcolibri.com/jstl-для-написания-jsp-страниц/) + - JSTL: Шаблоны для разработки веб-приложений в java + - JSTL LocalDateTime format + +### Optional +#### 2. Реализуем в ПАМЯТИ CRUD (create/read/update/delete) для еды +**Пример: Simple CRUD using Servlet/JSP** +- 2.1 Хранение в памяти будет одна из наших CRUD реализаций (позже будет JDBC, JPA и DATA-JPA). +- 2.2 Работать с реализацией CRUD через интерфейс, который не должен ничего знать о деталях реализации (Map, DB или что-то еще). +- 2.3 Добавить поле `id` в `Meal/ MealTo` и реализовать генерацию счетчика, УЧЕСТЬ МНОГОПОТОЧНОСТЬ СЕРВЛЕТОВ + - [обзор java.util.concurrent](https://habrahabr.ru/company/luxoft/blog/157273/) +- 2.4 Сделать форму редактирования в JSP: AJAX/JavaScript использовать НЕ надо, делаем через `
` и `doPost()` в сервлете. +- 2.5 Для ввода дат и времени можно использовать html5 типы, хотя они поддерживаются не всеми браузерами (протестировать свой браузер). В конце курса мы добавим DateTimePicker jQuery plugin, который будет работать на всех браузерах. + +## После выполнения ДЗ обязательно проверьте решение на ошибки + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW1 + +> Не попадаю на страничку/брекпойнт в сервлете. + +- внимательно проверь url и applicationContext (Application Context должен быть тот же, что и url приложения: wiki IDEA) +- посмотрите в task manager: возможно запущено несколько JVM и они мешают друг другу. Лишние java приложения убить. + +> Приложение не видит TOPJAVA_ROOT. + +**После выставления переменной окружения IDEA нужно рестартовать. Слеши в пути должны быть в стиле unix (/)**. Проверить, видит ли java переменную окружения можно так: `System.getenv("TOPJAVA_ROOT")`. Еще вариант: добавить `-DTOPJAVA_ROOT=...` в опции запуска приложения, тогда она доступна из java как `System.getProperty("TOPJAVA_ROOT")`. + +> Проблемы с кодировкой в POST (кракозябры). + +Возможное решение - выставьте кодировку ДО первого чтения из request: +``` +protected void doPost(HttpServletRequest request, ...) { + request.setCharacterEncoding("UTF-8"); +``` + +> Если сервлет тыкают несколько пользователей / несколько браузеров, какого должно быть поведение? Нужно ли что-то делать с сессиями? + +В Optional нужно делать реализацию хранения многопоточной. Cессии пока не используем (начнутся, когда будет прикручивать авторизацию). + +> Для чего нам нужна многопоточная реализация коллекции, если каждый пользователь видит только себя? + +Реализация хранения в памяти у нас одна на всех. Те коллекция шарится между пользователями, а они в разных потоках ее дергают. Если несколько потоков одновременно будут изменять коллекцию без учета многопоточности (например один будет удалять, второй вставлять), мы получим `ConcurrentModificationException` + +> Предпочтительнее ли создавать новый объект `Meal` при каждом update? + +Если при обновлении не создавать копию, то сохраненный в памяти объект может кто-то попортить. Вопрос скорее доверия к коду- если проект большой и людей над ним трудится много, то вероятнее нужно копировать. + +> Почему теряются атрибуты при передаче на сервлет: `http://localhost:8080/topjava/meals?action=add&...` и `req.getAttribute("action")` = null ? + +См. Difference between getAttribute() and getParameter(). Отсюда также следует, что при редиректе атрибуты теряются. + +> Зачем нужен в jsp `` ? + +jsp:useBean нужен IDEA для автодополнений - она понимает тип переменной, которая уже доступна в JSP (например через setAttribute). И еще эта переменная становится доступной в java вставках. Для вывода в JSP это тэг не обязателен. Если тип переменной JSP не совпадает с тем, что в `jsp:useBean`, будет ошибка. + +---------------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки +- 1 **Если в названии класса есть `Meal`, не нужно использовать слово meal в методах класса.** +- 2 Привыкайте писать комментарии к чекину: одной фразой что вы сделали в нем. Например: *Meals CRUD implementation*. См. +[Как писать сообщения коммитов](http://frontiermag.ru/commit-message.html) +- 3 Хранение в памяти и операции с ней должны выполняться просто и эффективно +- 4 Хранить нужно `Meal` и конвертировать ее в `MealTo` когда отдаем список на отображение в JSP. Иначе при редактировании любой записи или изменении юзером своей нормы `caloriesPerDay` нужно будет пересчитывать все записи юзера. +- 5 Стили `color` можно применять ко всей строке таблицы `tr`, а не каждой ячейке. +- 6 `DateTimeFormatter` можно сделать один заранее (он потокобезопасный в отличие от `SimpleDateFormatter`), а не создавать новый при каждом запросе. +- 7 Работать с CRUD надо через интерфейс. +- 8 Реализаций хранения будет несколько, нужно учитывать это в названии класса имплементации работы в памяти. +- 9 В `web.xml` принято группировать сервлет со своим маппингом +- 10 Не размещайте никакую логику (форматирование, счетчики) в бинах, где хранятся только данные (`Meal, MealTo`) +- 11 Еще раз: детали реализации в памяти не должны быть никому видны. Те НЕ НАДО счетчик размещать в `Meal` или `MealServlet` или `MealsUtil`, в базе же он будет по другому генерится. +- 12 `volatile` при ++ не помогает от многопоточности. Почему? +- 13 Обратите также внимание на то, чтобы реализация вашей коллекции для хранения еды была также многопоточной. +- 14 Не делайте дублирование кода `MealsUtil`. Возможно вам пригодятся константы `LocalTime.MIN` и `LocalTime.MAX` +- 15 Не дублируйте строки в `jsp`. Посмотрите на тернарный оператор. +- 16 после операции `delete` в браузере должен быть url `http:\\localhost:8080\topjava\meals` +- 17 Перед чекином проверяйте свой ченджлист (`Ctrl+D` на файле из `Local Changes` - посмотреть что поменялось). Если там только пробелы/переводы строк, не надо его комитить - делайте файлу `Git->revert`. +- 18 Учтите в названии реализации CRUD, что + - 18.1 у нас будет несколько реализаций (не только в памяти) + - 18.2 у нас будет 2 CRUD (для еды и пользователей). А в реальном проекте их намного больше. +- 19 Сессии НЕ использовать! При редиректе все атрибуты (`req.getAttribute()`) теряются (см. вопрос выше). Сценарий редиректа: + - 1 из сервлета делаем редирект (снова на сервлет, не на JSP!) + - 2 снова заходим в сервлет + - 3 кладем нужные атрибуты и делаем forward на jsp + - 4 если очень хочется передать параметры из 1. в 2. можно сделать их через параметры запроса (например `meals?id=5`) и доставать через `reg.getParameter(id)`. В моей реализации такого не потребовалось. diff --git a/doc/lesson02.md b/doc/lesson02.md new file mode 100755 index 000000000000..644346f3a890 --- /dev/null +++ b/doc/lesson02.md @@ -0,0 +1,251 @@ +# Онлайн проект Topjava + +## Материалы занятия (скачать все патчи можно через Download папки patch) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW1: + +- **Перед сборкой проекта (или запуском Tomcat) откройте вкладку Maven Projects и сделайте `clean`** +- **Если страничка в браузере работает неверно, очистите кэш (`Ctrl+F5` в хроме)** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Отображения списка еды в JSP +#### Apply 2_1_HW1.patch + +> - Изменения в `MealsUtil`: +> - Сделал константу `List MEALS` +> - Сделал вспомогательный метод `getWithExcess`. Для фильтрации передаю реализацию `Predicate` (см. паттерн [Стратегия](https://refactoring.guru/ru/design-patterns/strategy)) +> - Форматирование даты сделал на основе JSTL LocalDateTime format +> - Переименовал `TimeUtil` в `DateTimeUtil` +> - Переименовал `mealList.jsp` в `meals.jsp` +> - Добавил еще один способ вывести `dateTime` через стандартную JSTL функцию `replace` (префикс `fn` в шапке также надо поменять) + +- [jsp:useBean](http://www.labir.ru/j2ee/jspUseBean.html) +- [MVC - Model View Controller](http://design-pattern.ru/patterns/mvc.html) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Optional: реализация CRUD +#### Apply 2_2_HW1_optional.patch +Про использование паттерна Repository будет подробно рассказано в видео "Слои приложения" + +> - Поправил `InMemoryMealRepositoryImpl.save()`. Если обновляется еда, которой нет в хранилище (c несуществующим id), вставка не происходит. +> - В `MealServlet.doGet()` сделал выбор через `switch` +> - В местах, где требуется `int`, заменил `Integer.valueOf()` на `Integer.parseInt()` +> - В `meal.jsp` используется параметр запроса `param.action`, который не кладется в атрибуты. +> - Переименовал `mealEdit.jsp` в `mealForm.jsp`. Поля ввода формы добавил `required` +> - Пофиксил багу c `history.back()` в `mealForm.jsp` для **FireFox** (коммит формы при Cancel, сделал `type="button"`). + +Дополнительно: + - HTTP 1.0 vs 1.1 + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW1 + +> Зачем в `InMemoryMealRepositoryImpl` наполнять map с помощью нестатического блока инициализации, а не в конструкторе? + +Разницы нет. Сделал чтобы напомнить вам про эту конструкцию. [Малоизвестные особенности Java](https://habrahabr.ru/post/133237/) + +> Почему `InMemoryMealRepositoryImpl` не singleton? + +Начиная с Servlet API 2.3 пул сервлетов не создается, [создается только один инстанс сервлетов](https://stackoverflow.com/questions/6298309). Те. `InMemoryMealRepositoryImpl` в нашем случае создается тоже только один раз. Далее все наши классы слоев приложения будут создаваться через Spring, бины которого по умолчанию являются синглтонами (в его контексте). + +> `Objects.requireNonNull` в `MealServlet.getId(request)` если у нас нет `id` в запросе бросает NPE (`NullPointerException`). Но оно вылетит и без этого метода. Зачем он нужен и почему мы его не обрабатываем? + +`Objects.requireNonNull` - это проверка предусловия (будет подробно на 4-м занятии). Означает что в метод пришел неверный аргумент (должен быть не null) и приложение сообщает об ошибке сразу на входе (а не "может быть где-то потом"). См. [What is the purpose of Objects#requireNonNull](https://stackoverflow.com/a/27511204/548473). Если ее проглатывать или замазывать, то приложение возможно где-то работает неверно (приходят неверные аргументы), а мы об этом не узнаем. Красиво обрабатывать ошибки будем на последних занятиях (Spring Exception Handling). + +## Занятие 2: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava +- Apache Commons, Guava + - Guava используется на проекте [Многомодульный maven. Многопоточность. XML (JAXB/StAX). Веб сервисы (JAX-RS/SOAP). Удаленное взаимодействие (JMS/AKKA)](http://javaops.ru/reg/masterjava) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Слои приложения. Создание каркаса приложения. +#### Apply 2_3_app_layers.patch +> - Переименовал `ExceptionUtil` в `ValidationUtil` +> - Поменял `LoggedUser` на `SecurityUtil`. Это класс, из которого приложение будет получать данные авторизированного пользователя (пока авторизации нет, он реализован как заглушка). Находится в пакете `web`, т.к. авторизация происходит на слое контроллеров и остальные слои приложения про нее знать не должны. +> - Добавил проверку id пользователя, пришедшего в контроллер ([treat IDs in REST body](https://stackoverflow.com/a/32728226/548473), "If it is a public API you should be conservative when you reply, but accept liberally") + +![Слои приложения](http://4.bp.blogspot.com/-B4BdrPHfILA/Tu6dC5uK4dI/AAAAAAAAAeQ/iPEM0WctR7Y/s1600/Roo+Technical+Architecture+Diagram.png) + +- Паттерн "Слои приложения" +- Data Access Object +- Паттерн DTO +- Value Object и Data Transfer Object +- Should services always return DTOs, or can they also return domain models? +- Дополнительно: + - Паттерны Repository и DAO + - Забудьте о DAO, используйте Repository + - Difference between Active Record and DAO + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Какова цель деления приложения на слои? + +Управляемость проекта (особенно большого) повышается на порядок: +- Обеспечивается меньшая связываемость. Допустим если мы меняем что-то в контроллере, то сервис эти изменения не задевают. +- Облегчается тестирование (мы будем тестировать слои сервисов и контроллеров отдельно) + +> DTO используются когда есть избыточность запросов, которую мы уменьшаем, собрав данные из разных бинов в один? Когда DTO необходимо использовать? + +(D)TO может быть как частью одного entity (набор полей) так и набором нескольких entities. +В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например `MealsTo` мы отдаем наружу и он является Transfer Object, его надо перенести в пакет `to`. +На многих проектах (и собеседованиях) практикуют разделение на уровне maven модулей entity слоя от логики и соответствующей конвертацией ВСЕХ Entity в TO, даже если у них те же самые поля. +Хороший ответ когда TO обязательны есть на stackoverflow: When to Use. + +> Почему контроллеры положили в папку web, а не в conrollers? + +То же самое что `domain/model` - просто разные названия. + +> Зачем мы наследуем `NotFoundException` от `RuntimeException`? + +Так с ним удобнее работать. И у нас нет никаких действий по восстановлению состояния приложения (no recoverable conditions): checked vs unchecked exception. По последним данным checked exception вообще depricated: Ignore Checked Exceptions + +> Зачем в API пишем `NotFoundException`, если они `RuntimeException`? + +Обычно не пишут. Я написал для информации разработчикам - здесь делаем проверку и может быть брошено. + +> Зачем в `AdminRestController` переопределяются методы родителя с вызовом тех же родительских? + +Сделано на будущее, мы будем работать с `AdminRestController`. + +> И что такое `ProfileRestController`? + +Контроллер, где авторизованный пользователь будет работать со своими данными + +> Что лучше возвращать из API: `Collection` или `List` + +Вообще, как правило, возвращают `List`, если не просится по коду более общий случай (например возможный `Set` или `Collection`, возвращаемый `Map.values()`). Если возвращается отсортированный список, то `List` будет адекватнее. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Обзор Spring Framework. Spring Context. +#### Apply 2_4_add_spring_context.patch +- Spring Framework +- Проекты Spring. +- Обзор Spring Framework + +#### Apply 2_5_add_dependency_injection.patch +- Инверсия управления. +- IoC, DI, IoC-контейнер — Просто о простом + +#### Apply 2_6_add_annotation_processing.patch +> - Закомментировал ненужный `context:annotation-config`: сканирование аннотаций подключаются при `context:component-scan` + +- Difference + between @Component, @Repository & @Service annotations in Spring +- Spring Auto Scanning Components +- Использование аннотации @Autowired +- Дополнительное: + - Introduction to the Spring IoC container + and beans + - Constructor против Setter Injection + - Getting Started + - Spring Framework Reference Documentation + - Spring на GitHub + - Spring Annotations + +#### Apply 2_7_constructor_injection.patch +- [Inject 2 beans of same type](https://stackoverflow.com/a/2153680/548473) +- [Перевод "Field Dependency Injection Considered Harmful"](https://habrahabr.ru/post/334636/) +- [Tutorial: testing with AssertJ](http://www.vogella.com/tutorials/AssertJ/article.html) +- [Field vs Constructor vs Setter DI](http://stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it) +- [Implicit constructor injection for single-constructor scenarios](https://spring.io/blog/2016/03/04/core-container-refinements-in-spring-framework-4-3#implicit-constructor-injection-for-single-constructor-scenarios) + +В контроллерах *Constructor Injection* делать не стал, добавляется лишний код (попробуйте сделать сами). На каждом проекте свои правила, универсальных нет. + +#### Дополнительно видео по Spring + - [Юрий Ткач: Spring Framework - The Basics](https://www.youtube.com/playlist?list=PL6jg6AGdCNaWF-sUH2QDudBRXo54zuN1t) + - [Java Brains: Spring Framework](https://www.youtube.com/playlist?list=PLC97BDEFDCDD169D7) + - [Тимур Батыршинов: Spring](https://www.youtube.com/playlist?list=PLwwk4BHih4fho6gmaAwdHYZ6QQq0aE7Zi) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Пояснения к HW2. Обработка Autowired + +`` говорит спрингу при поднятии контекста обрабатывать `@Autowired` (добавляется в контекст спринга `AutowiredAnnotationBeanPostProcessor`). После того, как все бины уже в контексте постпроцессор через отражение инжектит все `@Autowired` зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке. + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Что такое схема в spring-app.xml xsi:schemaLocation и зачем она нужна + +XML схема нужна для валидации xml, IDEA делает по ней автозаполнение. + +> Что означает для Spring + + + + ? + +Можно сказать так: создай и занеси в свой контекст экземпляр класса (бин) `UserServiceImpl` и заинжекть в его проперти из своего контекста бин `inmemoryUserRepository`. + +> Как биндинг происходит для `@Autowired`? Как поступать, если у нас больше одной реализации `UserRepository`? + +`@Autowired` инжектит по типу (т.е. ижектит класс который реализует `UserRepository`). Обычно он один. Если у нас несколько реализаций, Spring не поднимится и поругается - `No unique bean`. + В этом случае можно уточнить имя бина через @Qualifier. `@Qualifier` обычно добавляют только в случае нескольких реализаций. + +> Почему нельзя сервлет помещать в Spring контекст? + +Сервлеты- это исключительно классы `servlet-api` (веб контейнера) и должны инстанциироваться и работать в нем. Те технически можно ( без `init/destroy`), но идеологически - неверно. Также НЕ надо работать с cервлетом из `SpringMain`. + +-------------------- +- **Еще раз смотрим на [демо приложение](http://topjava.herokuapp.com) и вникаем, что такое пользователь и его еда и что он может с ней сделать. Когда пользователь авторизуется в приложении, его id и норма калорий "чудесным образом" попадают в `SecurityUtil.authUserId()/authUserCaloriesPerDay()` и в приложении мы может обращаемся к ним. Как они реально туда попадут будет в уроке 9 (Spring Security, сессия и куки)** +- **Перед началом выполнения ДЗ (если есть хоть какие-то сомнения) прочитайте ВСЕ ДЗ. Если вопросы остаются- то ВСЕ подсказки**. Особенно этот пункт важный, когда будете делать реальное рабочее ТЗ. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW02 +- 1: Имплементировать `InMemoryUserRepositoryImpl` по аналогии с `InMemoryMealRepositoryImpl` (список пользователей возвращать отсортированным по имени) +- 2: сделать `Meal extends AbstractBaseEntity`, `MealTo` перенести в пакет `ru.javawebinar.topjava.to` (transfer objects) +- 3: Изменить `MealRepository` и `InMemoryMealRepositoryImpl` таким образом, чтобы вся еда всех пользователей находилась в одном общем хранилище, но при этом каждый конкретный авторизованный пользователь мог видеть и редактировать только свою еду. + - 3.1: реализовать хранение еды для каждого пользователя можно с добавлением поля `userId` в `Meal` ИЛИ без него (как нравится). Напомню, что репозиторий один и приложение может работать одновременно с многими пользователями. + - 3.2: если по запрошенному id еда отсутствует или чужая, возвращать `null/false` (см. комментарии в `UserRepository`) + - 3.3: список еды возвращать отсортированный в обратном порядке по датам + - 3.4: атомарность операций не требуется (коллизии при одновременном изменении одного пользователя можно не учитывать) +- 4: Реализовать слои приложения для функциональности "еда". API контроллера должна удовлетворять все потребности демо приложения и ничего лишнего (см. [демо](http://topjava.herokuapp.com)). + - **Смотрите на реализацию слоя для user и делаете по аналогии! Если там что-то непонятно, не надо исправлять или делать по своему. Задавайте вопросы. Если действительно нужна правка- я сделаю и напишу всем.** + - 4.1: после авторизации (сделаем позднее), id авторизованного юзера можно получить из `SecurityUtil.authUserId()`. Запрос попадает в контроллер, методы которого будут доступны снаружи по http, т.е. запрос можно будет сделать с ЛЮБЫМ id для еды + (не принадлежащем авторизированному пользователю). Нельзя позволять модифицировать/смотреть чужую еду. + - 4.2: `SecurityUtil` может использоваться только на слое web (см. реализацию `ProfileRestController`). `MealService` можно тестировать без подмены логики авторизации, принимаем в методах сервиса и репозитория параметр `userId`: id владельца еды. + - 4.3: если еда не принадлежит авторизированному пользователю или отсутствует, в `MealServiceImpl` бросать `NotFoundException`. + - 4.4: конвертацию в `MealTo` можно делать как в слое web, так и в service ([Mapping Entity->DTO: Controller or Service?](http://stackoverflow.com/questions/31644131)) + - 4.5: в `MealServiceImpl` постараться сделать в каждом методе только одни запрос к `MealRepository` + - 4.6 еще раз: не надо в названиях методов повторять названия класса (`Meal`). +- 5: включить классы еды в контекст Spring (добавить аннотации) и вызвать из `SpringMain` любой метод `MealRestController` (проверить что Spring все корректно заинжектил) + +### Optional +- 6: в `MealServlet` сделать инициализацию Spring, достать `MealRestController` из контекста и работать с едой через него (как в `SpringMain`). `pom.xml` НЕ менять, работаем со `spring-context`. Сервлет обращается к контролеру, контроллер вызывает сервис, сервис - репозиторий. + - 6.1: учесть, что когда будем работать через Spring MVC, `MealServlet` удалим, те вся логика должна быть в контроллере +- 7: добавить в `meals.jsp` и `MealServlet` две отдельные фильтрации еды: по дате и по времени (см. [демо](http://topjava.herokuapp.com)) +- 8: добавить выбор текущего залогиненного пользователя (имитация авторизации, сделать Select с двумя элементами со значениями 1 и 2 в `index.html` и `SecurityUtil.setAuthUserId(userId)` в `UserServlet`). +Настоящая атворизация будет через Spring Security позже. +--------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW02 (для проверки, сначала сделайте самостоятельно!) + +- 1: **В реализации `InMemoryUserRepositoryImpl`** + - 1.1: `getByEmail` попробуйте сделать через `stream` + - 1.2: `delete` попробуйте сделать за одно обращение к map (без `containsKey`) + - 1.3: предусмотрите случай одинаковых `User.name` (порядок должен быть зафиксированным). +- 2: **В реализации `InMemoryMealRepositoryImpl`** + - 2.1: В `Meal`, которая приходит в контроллер нет никакой информации о пользователе (еда приходит в контроллер БЕЗ `user/userId`). Она может быть только авторизованного пользователя, поэтому что-то сравнивать с ним никакого смысла нет. По `id` еды и авторизованному пользователю нужно проверить ее принадлежность. + **Проверьте сценарий: авторизованный пользователь пробует изменить чужую еду (id еды ему не принадлежит).** + - 2.2: `get\update\delete` - следите, чтобы не было NPE (`NullPointException` может быть, если в хранилище отсутствует юзер или еда). + - 2.3: Фильтрацию по датам сделать в репозитории т.к. из базы будем брать сразу отфильтрованные по дням записи. Следите чтобы **первый и последний день не были обрезаны, иначе сумма калорий будет неверная**. + - 2.4: Если запрашивается список и он пустой, не возвращайте NULL! По пустому списку можно легко итерироваться без риска `NullPoinException`. + - 2.5: Не дублируйте код в `getAll` и метод с фильтрацией + - 2.6: попробуйте учесть, что следующая реализация (сортировка, фильтрация) будет делаться прямо в базе данных +- 3: Проверьте, что удалили `Meal.id` и связанные с ним методы (он уже есть в базовом `BaseEntity`) +- 4: Проверку `isBetweenDate` сделать в `DateTimeUtil`. Попробуйте использовать дженерики и объединить ее с `isBetweenTime` (см. [Generics Tutorials](https://docs.oracle.com/javase/tutorial/extra/generics/morefun.html)) +- 5: **Реализация 'MealRestController' должен уметь обрабатывать запросы**: + - 5.1: Отдать свою еду (для отображения в таблице, формат `List`), запрос БЕЗ параметров + - 5.2: Отдать свою еду, отфильтрованную по startDate, startTime, endDate, endTime + - 5.3: Отдать/удалить свою еду по id, параметр запроса - id еды. Если еда с этим id чужая или отсутствует - `NotFoundException` + - 5.4: Сохранить/обновить еду, параметр запроса - Meal. Если обновляемая еда с этим id чужая или отсутствует - `NotFoundException` + - 5.5: Сервлет мы удалим, а контроллер останется, поэтому возвращать `List` надо из контроллера. И userId принимать в контроллере НЕЛЬЗЯ (иначе - для чего аторизация?). Подмену `MIX/MAX` для `Date/Time` также сделайте здесь. + - 5.6: В REST при update принято передавать id (см. `AdminRestController.update`) + - 5.7: Сделайте отдельный `getAll` без применения фильтра +- 6: Проверьте корректную обработку пустых значений date и time в контроллере +- 7: `id` авторизированного пользователя получаем так: `SecurityUtil.authUserId()`, cм. `ProfileRestController` +- 8: В `MealServlet` + - 8.1: Закрывать springContext в сервлете грамотнее всего в `HttpServlet.destroy()`: если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглтон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно. + - 8.2: Не храните параметры фильтра как члены класса сервлета, это не многопоточно! Один экземпляр сервлета используется всеми запросами на сервер, попробуйте дернуть его из 2х браузеров. + +#### Если с ДЗ большие сложности, можно получить итоговые Meal интерфейсы для сверки в личке (у меня, Татьяны, Катерины). +И напоследок история от Татьяны: +> 2.1 По id еды и авторизованному пользователю нужно проверить ее принадлежность. + +На примере уточню: +Вася Пупкин нашел неименную банковскую карточку, т.е. номер есть, но имени нет. +Т.к. Вася не очень честный человек, то решил снять деньги с чужой карточки. +Наклеил *стикер со своим именем* на карточку и пришел в банк. Дает свой паспорт и карточку операционисту и просит снять всю наличность. +Варианты: +1. операционист сверяет стикер на карточке и паспорт - все ок, Вася получает наличность +2. операционист не обращает внимания на стикер, *а делает запрос в БД по номеру и сверяет ФИО в БД с паспортом* - ФИО не совпали, Вася в пролете + +Кто и так это понимает, тому небольшой спойлер. А кто не понимает, может, переспросят, пообсуждают. diff --git a/doc/lesson03.md b/doc/lesson03.md new file mode 100755 index 000000000000..0ca7d6bbf847 --- /dev/null +++ b/doc/lesson03.md @@ -0,0 +1,272 @@ +# Онлайн проект Topjava + +## [Материалы занятия](https://drive.google.com/drive/u/0/folders/0B9Ye2auQ_NsFT1NxdTFOQ1dvVnM) (скачать все патчи можно через Download папки patch) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW02 +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW2 +> **ВНИМАНИЕ! При удалении класса из исходников, его скомпилированная версия все еще будет находиться в target (и classpath). В этом случае (или в любом другом, когда проект начинает глючить) сделайте `mvn clean`.** + +#### Apply 3_01_HW2_repository.patch +> - В репозиториях по другому инстанциировал компараторы. [Оптимизация анонимных классов](http://stackoverflow.com/questions/19718353) не требуется! Почитайте комменты от Holger: *Java 8 relieves us from the need to think about such things at all*. +> - Зарефакторил `> DateTimeUtil.isBetween(T value, T start, T end)`. Метод теперь не зависит от date/time, перенес его в общий `Util` класс. Дженерики означают, что мы принимаем экземпляры класса, реализующего компаратор, который умеет сравнивать T или суперклассы от T. +> - Для фильтрации в `InMemoryMealRepositoryImpl` передаю `Predicate` анологично решению в `MealsUtil` +> - В `InMemoryMealRepositoryImpl.save()` вместо 2-х разнесенных по времени операций +> - `get(meal.getId(), userId)` +> - `meals.put(meal.getId(), meal)`, +между которыми может вклинится операция удаления этой еды из другого потока, сделал обновление атомарным, используя `ConcurrentHashMap.computeIfPresent()` (см. псевдокод в `Map.computeIfPresent`. `ConcurrentHashMap` в отличие от `HashMap` делает операции атомарно). + +#### Apply 3_02_HW2_meal_layers.patch +> - убрал дублирование в `InMemory` реализациях: +> - сделал базовый `InMemoryBaseRepositoryImpl` +> - наследую от него `InMemoryUserRepositoryImpl` +> - использую его в `InMemoryMealRepositoryImpl` вместо `Map` +> - обратите внимание на `InMemoryBaseRepositoryImpl.counter` - счетчик один, общий для всех хранимых объектов +> - перенес обработку null-дат в `Util.isBetween` и `DateTimeUtil.adjustDateTime`. Добавил [Spring `@Nullable` аннотации](https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html) +> - по аналогии с `AbstractUserController` добавил проверку id пользователя, пришедшего в `MealRestController (assureIdConsistent, checkNew)` +> - поправил интерфейс `MealService.update`: контроллер и сервис при `update` ничего не возвращает + +#### Apply 3_03_HW2_optional_MealServlet.patch +> - Убрал логирование (уже есть в контроллере) +> - `assureIdConsistent` позволяет в контроллере обновлять еду с `id=null` + +#### Apply 3_04_HW2_optional_filter.patch +> - Вместо `MealServlet.resetParam` (перемещение параметров фильтрации в атрибуты запроса для отображения в `meals.jsp`), [достаю их в jsp напрямую из запроса через `${param.xxx}`](https://stackoverflow.com/a/1890462/548473) +> - В демо фильтр не хранится в сессии (скидывается по F5). Что такое сессия будем разбирать, когда будем делать реальную авторизацию +> - Цвет строк сделал через аттрибут `data-mealExcess` и css `tr[data-mealExcess=...]` +> - [Использование data-* атрибутов](https://developer.mozilla.org/ru/docs/Web/Guide/HTML/Using_data_attributes) + +#### Apply 3_05_HW2_optional_select_user.patch + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Вопросы по API и слоям приложения +- Should services always return DTOs, or can they also return domain models? +- Mapping Entity->DTO goes in which application layer: Controller or Service? + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW2 + +> Что делает `repository.computeIfAbsent / computeIfPresent` ? + +Всегда пробуйте ответить на вопрос сами. Дастоточно просто зайти по Ctrl+мышка в реализацию и посмотреть javadoc и **их дефолтную реализацию** + +> Почему выбрана реализация `Map>` а не `Meal.userId + Map` ? + +В данном случае двойная мапа (мультимап) - самый эффективный способ хранения, который не требует итерирования (перебора всех значений). С другой стороны затраты по памяти больше, поэтому решение не подходит, если юзеров много. + +## Занятие 3: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Коротко о жизненном цикле Spring контекста. +#### Apply 3_06_bean_life_cycle.patch +> На JDK 11 перейдем в 5 занятии. Если у вас JDK больше 8, [добавьте в pom `javax.annotation-api`](https://stackoverflow.com/a/46502132/548473) + +- Spring изнутри. Этапы инициализации контекста. +- Ресурсы: + - Евгений Борисов. Spring, часть 1 + - Евгений Борисов. Spring, часть 2 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Тестирование через JUnit. +### ВНИМАНИЕ!! Перед накаткой патча создайте каталог test (из корня проекта путь `\src\test`), иначе часть файлов попадет в `src\main`. + +> - в `maven-surefire-plugin` (JUnit) поменял кодировку на UTF-8 +> - добавил метод `InMemoryUserRepositoryImpl.init()` для инициализации. +> - переименовал тестовые классы + +#### Apply 3_07_add_junit.patch +### После патча сделайте `clean` и [обновите зависимости Maven](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C-%D0%B7%D0%B0%D0%B2%D0%B8%D1%81%D0%B8%D0%BC%D0%BE%D1%81%D1%82%D0%B8-%D0%B2-maven-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B5), чтобы IDEA определила сорсы тестов +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: почему проект упадет при попытке открыть страничку еды (в логе смотреть самый верх самого нижнего исключения)? +- JUnit 4 +- Тестирование в Java. JUnit + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Spring Test +> - поменял `@RunWith`: `SpringRunner` is an alias for the `SpringJUnit4ClassRunner` +#### Apply 3_08_add_spring_test.patch +- Spring Testing + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Базы данных. Обзор NoSQL и Java persistence solution без ORM. +- PostgreSQL. +- [PostgreSQL JDBC Driver](https://github.com/pgjdbc/pgjdbc) +- Установка PostgreSQL. **ВНИМАНИЕ! с Postgres 11 возможны проблемы.** +- Чтобы избежать проблем с правами и именами каталогов, [**рекомендуют установить postgres в простой каталог, например `C:\Postgresql`**. И при проблемах создать каталог data на другом диске](https://stackoverflow.com/questions/43432713/postgresql-installation-on-windows-8-1-database-cluster-initialisation-failed). Если Unix, [проверить права доступа к папке (0700)](http://www.sql.ru/forum/765555/permissions-should-be-u-rwx-0700). + +> Создать в pgAdmin новую базу `topjava` и новую роль `user`, пароль `password` + +![image](https://cloud.githubusercontent.com/assets/13649199/18809406/118f9c48-8283-11e6-8f10-d8291517a497.png) + +> Проверьте, что у user в Privileges есть возможность авторизации (особенно для pgAdmin4) + +или в UNIX командной строке: +``` +sudo -u postgres psql +CREATE DATABASE topjava; +CREATE USER "user" WITH password 'password'; +GRANT ALL PRIVILEGES ON DATABASE topjava TO "user"; +``` +- NoSQL or RDBMS. Обзор NoSQL систем. CAP +- DB-Engines Ranking +- JDBC +- Обзор Java persistence solution без ORM: commons-dbutils, + Spring JdbcTemplate, + MyBatis, JDBI, jOOQ +- Основы: + - Реляционная СУБД + - Реляционные базы + - Уроки по JDBC + - Postgres Guide + - PostgreSQL Tutorial + - Try SQL + - Базы данных на Java + - Возможности JDBC — второй этап +- Дополнительно: + - [Документация к PostgreSQL 9.6](https://postgrespro.ru/docs/postgresql/9.6/index.html) + - [Книги по PostgreSQL](https://postgrespro.ru/education/books) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Настройка Database в IDEA. +> Если проблемы к коннекшеном Heroku (`could not be verified by hostnameverifier PgjdbcHostnameVerifier`) к `database.url` добавьте `&sslmode=require` +#### Apply 3_09_add_postgresql.patch +- Настройка Database в IDEA и запуск SQL. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Скрипты инициализации базы. Spring Jdbc Template. +> в `JdbcUserRepositoryImpl.getByEmail()` заменил `queryForObject()` на `query()`. Загляните в код: `queryForObject` бросает `EmptyResultDataAccessException` вместо нужного нам `null`. + +#### Apply 3_10_db_implementation.patch +> - в `JdbcUserRepositoryImpl.save()` добавил проверку на несуществующей в базе `User.id` +> - в классе `JdbcTemplate` есть настройки (`queryTimeout/ skipResultsProcessing/ skipUndeclaredResults`) уровня приложения (если они будут менятся, то, скорее всего, везде в приложении). + Мы можем дополнительно сконфигурировать его в `spring-db.xml` и использовать в конструкторах `NamedParameterJdbcTemplate` и в `SimpleJdbcInsert` вместо `dataSource`. + +- Подключение Spring Jdbc. +- Конфигурирование DataSource. Property Placeholder + +> Проверьте, что в контекст Spring проекта включены оба файла конфигурации + +![image](https://cloud.githubusercontent.com/assets/13649199/24730713/eb21456a-1a6d-11e7-997c-fb4ad728ba45.png) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Тестирование UserService через AssertJ. +#### Apply 3_11_test_UserService.patch +> - В конструктор `User` внес `registered` и делаю копию `roles`, чтобы роли нельзя было изменить после инициализации. + +- [Spring Testing Annotations](https://docs.spring.io/spring/docs/4.3.x/spring-framework-reference/htmlsingle/#integration-testing-annotations-spring) +- [The JPA hashCode() / equals() dilemma](https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma) +- [Hibernate: implementing equals() and hashCode()](https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode) +- [Junit Matcher for comparators](https://stackoverflow.com/questions/17949752) +- [AssertJ custom comparison strategy](http://joel-costigliola.github.io/assertj/assertj-core-features-highlight.html#custom-comparison-strategy). [AssertJ field by field comparisons](http://joel-costigliola.github.io/assertj/assertj-core-features-highlight.html#field-by-field-comparison) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 10. Логирование тестов. +#### Apply 3_12_test_logging.patch +> - Новый PostgreSQL JDBC Driver [логирует через java.util.logging](https://github.com/pgjdbc/pgjdbc#changelog). [Направил логирование в SLF4J](http://stackoverflow.com/a/43242620/548473) +> - Поменял формат вывода. См. [Logback Layouts](https://logback.qos.ch/manual/layouts.html) + +- Ресурсы, которые кладутся в classpath, maven при сборке берет из определенных каталогов `resources` ([Introduction to the Standard Directory Layout](https://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html)). Их можно настраивать через [maven-resources-plugin](https://maven.apache.org/plugins/maven-resources-plugin/examples/resource-directory.html), меняем в проекте Masterjava. + +#### Apply 3_13_fix_servlet.patch +**Приложение перестало работать, тк. для репозитория мы используем заглушку `JdbcMealRepositoryImpl`** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 11. Ответы на Ваши вопросы +- Что такое REST? 10 Best Practices for Better RESTful API +- Зачем нужна сортировка еды? +- Можно ли обойтись без `MapSqlParameterSource`? +- Как происходит работа с DB в тестах? +- Как реализовывать RowMapper? +- Мои комментарии: решения проблем разработчиком. +- Нужен ли разработчику JavaScript? + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Какая разница между @BeforeClass and @Before? + +`@BeforeClass` выполняется один раз после загрузки класса (поэтому метод может быть только статический), `@Before` перед каждым тестом. +Также: для чистоты тестов экземпляр тестового класса пересоздается перед каждым тестом: http://stackoverflow.com/questions/6094081/junit-using-constructor-instead-of-before + +> Тесты в классе в каком-то определенном порядке выполняются ("сверху вниз" например)? + +Порядок по умолчанию неопределен, каждый тест должен быть автономен и не зависеть от других. См. также http://stackoverflow.com/questions/3693626/how-to-run-test-methods-in-specific-order-in-junit4 + +> Обязательно ли разворачивать postgreSQL? + +Желательно: хорошая и надежная ДБ:) Если совсем не хочется - можно работать со своей любимой RDBMS (поправить `initDB.sql`) или работать c postgresql в heroku (креденшелы к нему есть сверху в `postgres.properties`). На следующем уроке добавим HSQLDB, она не требует установки. + +> Зачем начали индексацию с 100000? + +Тут нет "как принято". Так удобно вставлять в базу (если будет потребность) записи вручную не мешая счетчику. + +> Из 5-го видео - "Логика в базе - большое зло". Можно чуть поподробней об этом? + +- Есть успешные проекты с логикой в базе. Те все относительно. +- Логика в базе - это процедуры и триггеры. Нет никакого ООП, переиспользовать код достаточно сложно, никагого рефакторинга, поиска по коду и других плюшек IDE. Нельзя делать всякие вещи типа кэширования, хранения в сесии - это все для логики на стороне java. Например json можно напрямую отдать в процедуру и там парсить и вставлять в таблицы или наоборот - собирать из таблиц и возвращать. +А затем потребуется некоторая логика на стороне приложения и все равно придется этот json дополнительно разпарсивать в java. +Я на таком проекте делал специальную миграцию, чтобы процедуры мигрировать не как sql скрипты, а каждую процедуру хранить как класс с историей изменений. Если логика: триггеры и простые процедуры записи-чтения, которые не требуют переиспользования кода или +проект небольшой это допустимо, иначе проект становится трудно поддерживать. Также иногда используют [View](http://postgresql.ru.net/gruber/ch20.html) для разграничения доступа. Например, для финансовых систем, таблицы проводок доступны только для админ учеток, а View просто не дадут увидеть (тем более изменить) данны обычному оператору на уровне СУБД. + +> У JUnit есть ассерты и у спринга тоже. Можно ли обойтись без JUnit? + +Предусловия и JUnit-тесты совершенно разные вещи. Один другого не заменит, у нас будут предусловия в следующем уроке. + +> Я так понял VARCHAR быстрее, чем TEXT, когда мы работаем с небольшими записями. Наши записи будут небольшими (255). Почему вы приняли решение перейти на TEXT? + +В отличие от MySql в Postgres VARCHAR и TEXT - тоже самое: http://stackoverflow.com/questions/4848964/postgresql-difference-between-text-and-varchar-character-varying + +> Зачем при создании таблицы мы создаем `CREATE UNIQUE INDEX` и `CREATE INDEX`. При каких запросах он будет использоваться? + +UNIQUE индекс нужен для обеcпечения уникальности, DB не даст сделать одинаковый индекс. Индексы используется для скорости выполнения запросов. Обычно они задействуются, когда в запросе есть условия, на которые сделан индекс. Узнать по конкретному запросу можно запросив план запроса: см. Оптимизация запросов. Основы EXPLAIN в PostgreSQL. На измерение производительности с индексами посмотрим в следующем уроке. + +> А это нормально, что у нас в базе у meals есть userId, а в классе - нет? + +Ненормально, когда в приложении есть "лишний" код, который не используется. Для ORM он нам понадобится- мы `Meal.user` добавим. + +> Почему мы делаем один sequence на разные таблицы? + +Мы будем использовать Hibernate, по умолчанию он делает глобальный sequence на все таблицы. В этом подходе есть как плюсы, так и минусы, из плюсов - удобно делать ссылки в коде и в таблицах на при наследовании и мапы в коде. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW03 +- 1 Понять, почему перестали работать `SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest` +- 2 Дополнить скрипты создания и инициализации базы таблицой MEALS. Запустить скрипты на вашу базу (через Run). Порядок таблиц при DROP и DELETE важен, если они связаны внешними ключами (foreign key, fk). Проверьте, что ваши скрипты работают +- 3 Реализовать через Spring JDBC Template `JdbcMealRepositoryImpl` + - 3.1. сделать каждый метод за один SQL запрос + - 3.2. `userId` в класс `Meal` вставлять НЕ надо (для UI и REST это лишние данные, userId это id залогиненного пользователя) + - 3.3. `JbdcTemplate` работает через сеттеры. Вместе с конструктором по умолчанию их нужно добавить в `Meal` + - 3.4. Cписок еды должен быть отсортирован (тогда мы его сможем сравнивать с тестовыми данными). Кроме того это требуется для UI и API: последняя еда наверху. +- 4 Проверить работу MealServlet, запустив приложение + +#### Optional +- 5 Сделать `MealServiceTest` из `MealService` и реализовать тесты для `JdbcMealRepositoryImpl`. +> По `Ctrl+Shift+T` (выбрать JUnit4) можно создать тест для конкретного класса, выбрав для него нужные методы. Тестовый класс создастся в папке `test` в том же пакете, что и тестируемый. + - 5.1 Сделать тестовые данные `MealTestData` (точно такие же, как вставляем в `populateDB.sql`). + - 5.2 Сделать тесты на чужую еду (delete, get, update) с тем чтобы получить `NotFoundException`. +- 6 Почнинить `SpringMain, InMemory*Test`. **InMemory тесты должны использовать реализацию в памяти** +- 7 Сделать индексы к таблице `Meals`: запретить создавать у одного и того-же юзера еду с одинаковой dateTime. +Индекс на pk (id) postgres создает автоматически: Postgres and Indexes on Foreign Keys and Primary Keys + - [PostgreSQL: индексы](https://postgrespro.ru/docs/postgresql/10/indexes-intro) + - Postgres Guide: Indexes + - [Оптимизация запросов. Основы EXPLAIN в PostgreSQL](https://habrahabr.ru/post/203320/) + - [Оптимизация запросов. Часть 2](https://habrahabr.ru/post/203386/) + - [Оптимизация запросов. Часть 3](https://habrahabr.ru/post/203484/) + +> ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Как правильно придумать индекс для базы? Указать в нем все поля, комбинация которых создает по смыслу уникальную запись, или какие-то еще есть условия? + +Индекс нужно делать по тем полям, по которым будут искаться записи (участвуют в WHERE, ORDER BY). Уникальность - совсем не обязательное условие. Индексы ускоряют поиск по определенным полям таблицы. Они не бесплатные (хранятся в памяти, замедляется вставка), поэтому на всякий случай их делать не надо. Также не строят индексы на колонки с малым процентом уникальности (например поле "М/Ж"). Поля индекса НЕ КОММУТАТИВНЫ и порядок полей в описании индекса НЕОБХОДИМО соблюдать (в силу использования B-деревьев и их производных как поисковый механизм индекса). При построении плана запроса EXPLAIN учитывается количество записей в базе, поэтому вместо индексного поиска (Index Scan) база может выбрать последовательный (Seq Scan). Проверить, работают ли индексы можно отключив Seq Scan. Также см. Queries on the first field of composite index + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Решение проблем + +> Из каталога `main` не видятся классы/ресурсы в `test` + +Все что находится в `test` используется только для тестов и недоступно в основном коде. + +> Из `IDEA` не видятся ресурсы в каталоге `test` + +- Сделайте Reimport All в Maven окне + +![image](https://cloud.githubusercontent.com/assets/13649199/18831806/7e43bedc-83f0-11e6-97db-67d4e1a7599f.png) + +> В UserServiceImpl и MealServiceImpl подчеркнуты красным repository, ошибка: Could not autowire. There is more than one bean of 'MealRepository' type. + +- Spring test контекст не надо включать в Spring Facets проекта, там должны быть только `spring-app.xml` и `spring-db.xml`. Для тестовых контекстов поставьте чекбокс `Check test files` в Inspections. + +![image](https://cloud.githubusercontent.com/assets/13649199/18831817/8a858f22-83f0-11e6-837e-bf5317b33b3a.png) + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Проверка по HW03 (сначала сделайте самостоятельно!) + +- 1: В `MealTestData` еду делайте константами. Не надо `Map` конструкций! +- 2: SQL case-insensitive, не надо писать в стиле Camel. В POSTGRES возможны case-sensitive значения, их надо в кавычки заключать (обычно не делают). +- 3: ЕЩЕ РАЗ: `InMemory` тесты должны идти на `InMemory` репозитории +- 4: **Проверьте, что возвращает `JdbcMealRepositoryImpl` при обновлении чужой еды** +- 5: В реализации `JdbcMealRepositoryImpl` одним SQL запросом используйте возвращаемое `update` значение `the number of rows affected` +- 6: При тестировании не портите констант из `MealTestData` +- 7: Проверьте, что все, что относится к тестам, находится в каталоге `test` (не попадает в сборку проекта) +- 8: **Еще раз: в тестах проверять через `JUnit Assert` или использовать `assertThat().isEqualTo` нельзя: сравнение будет происходить через `equals`, который сравнивает объекты только по `id`. Мы не можем переопределять `equals` для объектов модели, тк будем использовать JPA (см. [The JPA hashCode() / equals() dilemma](https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma))** +- 9: НЕ делайте склейку SQL запросов вручную из параметров, только через `jdbcTemplate` параметры! См. [Внедрение_SQL-кода](https://ru.wikipedia.org/wiki/Внедрение_SQL-кода) +- 10: Напомню: `BeanPropertyRowMapper` работает через отражение. Ему нужны геттеры/сеттеры и имена полей должны "совпадать" с колонками `ResultSet` (Column values are mapped based on matching the column name as obtained from result set metadata to public setters for the corresponding properties. The names are matched either directly or by transforming a name separating the parts with underscores to the same name using "camel" case). diff --git a/doc/lesson04.md b/doc/lesson04.md new file mode 100755 index 000000000000..2962a90b2124 --- /dev/null +++ b/doc/lesson04.md @@ -0,0 +1,229 @@ +# Онлайн проект Topjava + +## Материалы занятия + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW3 + +> `SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest` починим в патче **4_5_create_inmemory_test_ctx.patch (видео 4)** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. JdbcMealRepositoryImpl + MealServiceTest +#### **Apply 4_1_HW3.patch** +> - В `JdbcUserRepositoryImpl` поменял `MapSqlParameterSource` на `BeanPropertySqlParameterSource` (поля для вставки определяются через отражение в бине и метаданные в SQL запросе). + В `JdbcMealRepositoryImpl` остается `MapSqlParameterSource`, т.к. в отсутствует `Meal.userId`. См. дополнительно [CombinedSqlParameterSource](https://www.codota.com/java/spring/scenarios/549bbb5dda0a9536b85ad5f3/org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource?tag=spring1b) +> - Новый Postgres драйвер поддерживает Java 8 Date and Time. Преобразования c `Timestamp` уже не нужны. +> - В meals добавил составной индекс `INDEX meals_unique_user_datetime_idx ON meals(user_id, date_time)` для повышения скорости запросов по этим полям + +**Примечание**: в ответе на [Why is SELECT * considered harmful?](https://stackoverflow.com/questions/3639861) есть случаи, когда она допустима (наш случай): `when "*" means "a row"` + +- POSTGRESQL: BETWEEN CONDITION +- **[Сравнение времени выполнения для разных индексов](meals_index.md)** + - На id как на primary key индекс создается автоматически. + - все запросы в таблицу meals у нас идут с `user_id` + - по полю `date_time` также есть запросы + мы по нему сортируем список результатов, те они- хорошие кандидаты для индексирования. + - следует иметь в виду, индексы ускоряют операции чтения, но замедляют вставку и удаление, поэтому необходим анализ в реальном приложении + - [Оптимизация запросов. Основы EXPLAIN в PostgreSQL](https://habrahabr.ru/post/203320/) + - [Оптимизация запросов. Часть 2](https://habrahabr.ru/post/203386/) + - [Оптимизация запросов. Часть 3](https://habrahabr.ru/post/203484/) + - [Документация Postgres: индексы](https://postgrespro.ru/docs/postgresql/9.6/indexes.html) + +#### **Apply 4_2_HW3_optional.patch** +> Удалил лишние `MealsUtil.MEALS` + +## Занятие 4: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Методы улучшения качества кода +- Сделайте интеграцию своего GitHub репозитория и подключите сверху своего [README.md](https://github.com/JavaWebinar/topjava/blob/master/README.md) ([Raw](https://raw.githubusercontent.com/JavaWebinar/topjava/master/README.md)) интергацию с + - Codacy Check code (проверка стиля и поиск багов в коде). В Codacy Issues + - отключил в настройках (remove pattern) проверку assert в JUnit (проверяем через матчеры) и [`meta http-equiv="content-type"` в JSP](https://stackoverflow.com/a/45440410/548473) + - сделал remove pattern на Cross Site Scripting ([XSS](https://ru.wikipedia.org/wiki/Межсайтовый_скриптинг)), будем делать защиту на последнем занятии + - Сборку и тесты Travis (результат выполнения тестов проекта) + - [Что такое travis-ci.org](https://habr.com/post/140344/) + - [Travis CI Tutorial](https://dzone.com/articles/travis-ci-tutorial-java-projects) + - Сборка Java проекта + - Сервис по проверке `maven` зависимостей VersionEye [закрыли](https://blog.versioneye.com/2017/10/26/the-start-of-a-new-journey). Ищу замену... +- Сделайте `push` для отображения результатов текущего состояния проекта. + +#### **Apply 4_3_improve_code.patch** +Для пояснения материала видео сделал проверку предусловий `Objects.requireNonNull` и `Assert.notNull`. В реальном проекте везде используются один подход. + +> - Перенес проверки предусловий `Assert` из `InMemory` репозиториев в сервисы +> - Добавил конфигурацию `.travis.yml` + +- Контрактное программирование, Программирование по контракту +- Comparison Preconditions in Java +- IDEA Settings -> Plugins -> Browse repositories... Add [QAPlug: PMD/FindBugs/Checkstyle/Hammurapi](https://qaplug.com/about/) + - Tools -> QAPlug -> Analyze Code... +- IDEA [Analyze | Inspect Code](https://www.jetbrains.com/help/idea/running-inspections.html) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Spring: инициализация и популирование DB +#### **Apply 4_4_init_and_populate_db.patch** +- [Инициализация базы при старте приложения](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#jdbc-initializing-datasource-xml) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Подмена контекста при тестировании +#### **Apply 4_5_create_inmemory_test_ctx.patch** +> Переименовал `mock.xml` в `inmemory.xml` + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. ORM. Hibernate. JPA. +Entity- класс (объект Java), который в ORM маппится в таблицу DB. + +> - ВНИМАНИЕ: патч меняет `postgres.properties`, в котором у вас возможно свои креденшелы к базе. +> - `hibernate-core` 5.2.x включает `hibernate-entitymanager` и `hibernate-java8`, Time API конверторы уже не нужны. +> - JPA support for Java 8 new date and time API +> - What's new in Hibernate 5? +> - JPA support for Java 8 new date and time API +> - [EL implementation provided by the container. In a Java SE you have to add an implementation as dependency to your POM file](http://hibernate.org/validator/documentation/getting-started/#unified-expression-language-el): добавил `javax.el` зависимость со `scope=provided` + +#### **Apply 4_6_add_jpa.patch** +> - **Внимание: при [настройке JPA в IDEA](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C-jpa) НЕ скачивайте библиотеку javaee.jar (и любую другую). Все зависимости в проект попадают только через Maven.** +> - Тесты и приложение ломаются. `MealServiceTest` починится после выполнения HW04 (`JpaMealRepositoryImpl`) +> - Если вы используете Java 9 и выше, то возникают проблемы с `JAXBException` (пакет `java.xml.bind`). [См. решение](https://www.concretepage.com/forum/thread?qid=531) +- Дополнительно: + - ORM. + - JPA и Hibernate в вопросах и ответах + - [Hibernate — о чем молчат туториалы](https://habr.com/ru/post/416851/) + - [Наследование в Hibernate: выбор стратегии](https://habrahabr.ru/post/337488/) + - JPA EntityManager: управляем сущностями + - [Field vs property access](http://stackoverflow.com/a/6084701/548473) + - Hibernate: введение и написания Hello world приложения + - [15 reasons why we need to choose Hibernate over JDBC](https://habiletechnologies.com/blog/reasons-to-choose-hibernate-over-jdbc) + - Mapping: описания модели Hibernate (hbm.xml/annotation). + - Hibernate. Другие ORM: TopLink, EсlipseLink, EBean (used in Playframework). + - JPA (wiki). JPA (english wiki). JPA Performance Benchmark + - Стратегии генерации PK + - hibernate-validator. JSR-303 -> JSR-349 + - Описание связей в модели. Ленивая загрузка объекта. + - JPA definitions + - Spring expressions: выражения в конфигурации + - HQL/ JPQL. + - Динамические запросы (которые формируются в коде): JPA Criteria API. Unified Queries for Java + - Using the Java 8 Date Time Classes with JPA + +#### **Apply 4_7_add_named_query_and_transaction.patch** + +- Транзакция. ACID. Уровни изоляции транзакций. +- Spring Transaction Management +- readOnly и Propagation.SUPPORTS +- `@Transactional` в тестах. Настройка EntityManagerFactory + +Справочник: + - Видео: Вячеслав Круглов — Как начинающему Java-разработчику подружиться со своей базой данных? + - Видео: Николай Алименков — Босиком по граблям Hibernate + - Стратегии работы с транзакциями + - Примеры работы с JPA + - Spring transaction propagation tutorial + - Getting Started with JPA + - Java Persistence + - Разделы по Java Persistence API + - Spring Framework transaction management + - Spring Persistence Tutorial + - Working with JPA Entity Objects + - Стратегии работы с транзакциями: Распространенные ошибки + - Принципы работы СУБД. MVCC + - MVCC + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Добавляем поддержку HSQLDB + +#### **Apply 4_8_add_hsqldb.patch** + +> - ВНИМАНИЕ: патч меняет `postgres.properties` +> - IDEA может `${jdbc.initLocation}` подчеркивать красным - тупит... + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> Есть несколько аналогичных "встроенных" баз данных. H2, HSQLDB, Derby, SQLite. Почему был выбран HSQLDB? + +Просто с ней приходилось работать. HSQLDB и H2 наиболее популярны, в новом курсе по spring-boot планирую использовать H2. +Здесь интересное краткое описание встраиваемых баз данных в Java. +В HSQLDB нет репликаций, кластеризации и объем данным ограничен несколькими TB. Для большого количества приложений она подходит и для продакшена. См. +- What is HSQLDB limitations? +- HSQLDB в режиме in-process + +> Чистого JPA не существует, т.е. это всего лишь интерфейс, спецификация? Говорим JPA, подразумеваем какой-то ORM фрэймворк? А что тогда используют чистый jdbc, Spring-jdbc, MyBatis? MyBatis не реализует JPA? + +ORM это технология связывания БД и объектов приложения, а JPA - это JavaEE спецификация (API) этой технологии. +Реализации JPA - Hibernate, OpenJPA, EclipceLink, но, например, Hibernate может работать по собственному API (без JPA, которая появиласть позже). Spring-JDBC, MyBatis, JDBI не реализуют JPA, это обертки к JDBC. Все ORM и JPA также реализованы поверх JDBC. + +> В зависимостях maven `hibernate-entitymanager` тянет за собой `jboss-logging`. Как будет происходить логгирование? + +How do you configure logging in Hibernate 4 to use SLF4J: в нашем проекте автоматически подхватывается `logback-classic`. + +> В чем преимущество Hibernate ? + +Hibernate (как любая ORM) реализует маппинг таблиц в объекты Java. Когда мы добавим роли к пользователю вы увидете, насколько код будет проще, чем в jdbc. Также см. 5 Reasons to Use JPA / Hibernate + +> Чем отличается `@Column(nullable = false)` от `@NotNull` и есть ли необходимость указывать обе аннотации ? + +`@Column(nullable = false)` это атрибуты колонки таблицы базы. `@NotNull` - это валидация, которая происходит в приложении перед вставкой в базу. Если колонка ненулевая, то `NOT NULL` объязательна. Валидация- опциональна. Также см. +@NotNull vs @Column(nullable = false) + +> почему мы в в бине `entityManagerFactory` не указали диалект базы данных? + +Он [автоматически определяется из `DataSource` драйвера](http://stackoverflow.com/a/39817822/548473) + +> В чем разница между `persist` и `merge` + +Подробный ответ со Stackovwrflow с объяснением разницы. Упрощенно: + - `merge`, в отличие от `persist`, если entity нет в текущей сессии, делает запрос в базу данных + - entity, переданный в `merge` не меняется. Нужно использовать возвращаемый результат + +> `em.merge` - при отсутствии старой записи (несуществующее `id`) создает новую. Те в `JpaUserRepositoryImpl` нарушается логика + +В Hibernate есть такая бага: https://hibernate.atlassian.net/browse/HHH-1661 +- [Hibernate unexpectedly issues INSERT instead of throwing the javax.persistence.OptimisticLockException, when a nonexistent entity is passed to merge()](https://stackoverflow.com/questions/34249483) +- [Should Hibernate Session#merge do an insert when receiving an entity with an ID?](https://stackoverflow.com/questions/21489300) + +Если это действительно наш критичный бизнес кейс (например с многопоточным удалением entity) нужно искать варианты обходного решения. +Если же это результат неверного запроса, то, мое мнение, можно это оставить как есть. + +> Почему в проекте транзакционность сделана в слое репозитория, а не сервиса? Транзакциями удобнее пользоваться на слое сервисов, так как здесь реализуется бизнес логика и бывает нужно делать несколько операций в одной транзакции. + +С классической точки зрения все транзакции действительно объявляются на уровне сервиса. Мы будем использовать в логике сервиса несколько запросов и тогда сделаем дополнительную транзакцию на методе сервисе. Новая транзакция при этом не создается (по умолчанию используется `Propagation.REQUIRED`, который поддерживают существующую), поэтому несколько `@Transactional` аннотаций ведут себя как одна. Я использую подход `spring-data-jpa` (будет на следующем занятии): в репозитории транзакции объявлять удобно, тк не надо думать о них в сервисах. + +-------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW4 + +- 1: Сделать из `Meal` Hibernate entity + - Hibernate Validator: @NotNull, @NotEmpty, @NotBlank + - Реализация ManyToOne +- 2: Имплементировать и протестировать `JpaMealRepositoryImpl` + +#### Optional + +- 3: Добавить в тесты `MealServiceTest` функциональность `@Rule`: + - 3.1: проверку Exception + - 3.2: вывод в лог времени выполнения каждого теста + - 3.3: вывод сводки в конце класса: имя теста - время выполнения +- JUnit @Rules +- замена ExpectedException + +--------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW4 +- 1: Тк. JPQL работает с объектами мы не можем использовать `userId` для сохранения. Можно сделать например так: + + User ref = em.getReference(User.class, userId); + meal.setUser(ref); + + При этом от `User` нам нужет только `id`. Создается lazy прокси над `id`, которая обращается к базе при запросе любого поля. Т.е. у нас запроса в базу за юзером не будет- проверьте по логам Hibernate + +**Внимание: проверять запросы Hibernate нужно через run. Если делаете debug и брекпойнт, то могут делаться лишние запросы к базе (дебаггер дергает `toString`)** + +- 2: В JPQL запросах можно писать: `m.user.id=:userId` +- 3: При реализации `JpaMealRepositoryImpl` предпочтительно не использовать `try-catch` в логике реализации. Но если очень хочется, то ловить только специфичекские эксепшены (пр. `NoResultException`), чтобы, например, при отсутствии коннекта к базе приложение отвечало адекватно. +- 4: Мы будем смотреть генерацию db скриптов из модели, для корректной генерации нужно в `Meal` добавить `uniqueConstraints` +- 5: При записи в базу через `namedQuery` валидация ентити не работает, только валидация в бд +- 6: Результат `AssertionError` печатает результаты через `toString`, который может не совпадать с полями сравнения. +- 7: Если нашему приложению `Meal.user` не требуется, не следует включать его в тесты. В следующем уроке мы потренируемся разными способами доставать зависимости `Meal.user` и `User.meals` +- 8: Старые версии IDEA тупят по поводу проверки `BETWEEN`. Обновитесь либо не обращайте внимания. + +-------------------------------- +Новая информация плохо оседает в голове, когда дается в виде патчей, поэтому, чтобы она стала "твоей" нужно еще раз проделать это самостоятельно. Домашнее задание на этом уроке небольшое, а полученных знаний уже достаточно, чтобы после его выполнения начинать делать выпускной проект, сделанный на нашем стеке. +## [Выпускной проект](graduation.md) +- Для проекта я взял реальное тестовое задание, поэтому жалоб не неясность формулировок принимать не буду- сделайте как поняли. Представьте, что это ваше тестовое задание на работу. +- Общение в канале Slack *#graduation* +- Ревью проекта входит в участие с проверкой домашних заданий (ревьюится один раз!). Отдать на ревью нужно до 16.05.2019 (если идешь на проект "Многомодульный maven. Многопоточность. XML (JAXB/StAX). + Веб сервисы (JAX-RS/SOAP). Удаленное взаимодействие (JMS/AKKA) (Masterjava)", то срок до 16.08.2019). +- По завершению ты сможешь занести этот проект в свое портфолио и резюме как собственный, без всяких оговорок. +- Обязательно проверяйся [по рекомендациям в конце выпускного](graduation.md#-Рекомендации) + +### Успехов в выполнении! diff --git a/doc/lesson05.md b/doc/lesson05.md new file mode 100755 index 000000000000..3f8684e10d45 --- /dev/null +++ b/doc/lesson05.md @@ -0,0 +1,225 @@ +# Онлайн проект Topjava + +## Материалы занятия + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Обзор JDK 9/11. Миграция Topjava с 1.8 на 11](http://javaops.ru/view/resources/jdk8_11) +> - Для запуска Tomcat под JDK11 не из IDEA проверьте переменную окружения `JAVA_HOME` +> - Проверить версию java в path просто: `java -version` + +#### Apply 5_0_jdk_11.patch +- [Добавил javax зависимости](https://stackoverflow.com/questions/48204141/replacements-for-deprecated-jpms-modules-with-java-ee-apis) +- Сделал создание коллекций через фабричные методы `List.of` +- Как пример в `InMemoryMealRepositoryImpl` использовал *local variable type inference* `var`. + - [26 рекомендаций по использованию типа var в Java](https://habr.com/ru/post/438206/) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW4 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Разбор вопросов +- Validate by RegExp +- Working with JPA Entity Objects +- Java Persistence/Relationships +- Использование ThreadLocal переменных +- Merge vs Persist +- Видео: работа в ZK с OpenJPA (в чем Hibernate хуже) +- Паттерн- открытие транзакции в фильтре и почему это bad-practice +- Sequence Strategies +- SequenceGenerator/IdentityGenerator in PostgreSql + +> `EntityManager` это по сути прокси обертка над Hibernate Session, которая создается каждый раз при открытии транзакции. + +- Дополнительно (ни разу не сталкивался): еще есть редкий случай ручного управления `@PersistenceContext(type = PersistenceContextType.EXTENDED)`, когда он используется в нескольних транзакциях (long-running session or session-per-conversation). + - Spring and PersistenceContextType.EXTENDED + - Transaction-scoped vs Extended Persistence + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW4: JPA. @Rule +#### Apply 5_1_HW4.patch +> - При сравнении еды тесты падают, тк Hibernate делает ленивую обертку к `user` и если происходит обращение к любому его полю кроме id вне транзакции бросается `LazyInitializationException`. +По логике приложения поле `user` в еде не нужно и мы не будем его отдавать наружу: в тестах исключаем `user` из сравнения. +> - IDEA ругается на **BETWEEN**, в `Meal` добавил `@SuppressWarnings("JpaQlInspection")`. Other warnings +> - Поменял реализацию `JpaMealRepositoryImpl.get()` (вместо `@NamedQuery`), реализация стали проще + +#### Apply 5_2_fix_hibernate_issue.patch +> - Из за [Hibernate bug with proxy initialization when using `AccessType.FIELD`](https://hibernate.atlassian.net/browse/HHH-3718) + в `JpaMealRepositoryImpl.get()` делался дополнительный запрос в базу для инициализации прокси `User` и мы делали хак: доступ к полю `AbstractBaseEntity.id` через `AccessType.PROPERTY`. + С версии `5.2.13.Final` загрузкой прокси при обращении к `id` управляется флагом `JPA_PROXY_COMPLIANCE` (по умолчанию запрос не делается) +> - [Call to id getter initializes proxy when using AccessType( "field" ): HHH-3718](https://hibernate.atlassian.net/browse/HHH-3718) +> - [According to JPA, a Proxy should be loaded even when accessing the identifier: HHH-12034](https://hibernate.atlassian.net/browse/HHH-12034) +> - Which is better, field or property access? +> - Поправил `equals()` с учетом Lazy проксирования +> - JPA hashCode()/equals() dilemma +> - Hibernate Proxy Pitfalls + +------------------------ + +> Переопределять `equals()/hashCode()` необходимо, если +> - использовать entity в `Set` (рекомендовано для many ассоциаций), либо как ключи в `HashMap` +> - использовать _reattachment of detached instances_ (те манипулировать одним Entity в нескольких транзакциях/сессиях). + +> [Implementing equals() and hashCode()](https://access.redhat.com/documentation/en-us/jboss_enterprise_application_platform/4.3/html/hibernate_reference_guide/persistent_classes-implementing_equals_and_hashcode) + +> Оптимально использовать уникальные бизнес поля, но обычно таких нет, и чаще всего, используются PK с ограничением, что он может быть `null` у новых объектов и нельзя объекты сравнивать через `equals() and hashCode()` в бизнес-логике (например тестах). + +> [equals() and hashcode() when using JPA and Hibernate](https://stackoverflow.com/questions/1638723) + +------------------------ + +> ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Почему над `AbstractBaseEntity` стоит `@Access(AccessType.FIELD)` ? Почему при запросе `user.id` нам не нужно вытаскивать его из базы? + +`AccessType.FIELD` делает доступ в `AbstractBaseEntity` и всех классах-наследниках по полям. При загрузке `Meal` Hibernate на основе поля `meal.user_id` делает ленивую прокcи к `User`, у которой нет ничего, кроме id. + +#### Apply 5_3_HW4_optional.patch + +> - Hibernate 5.2.x already include Java 8 date and time types (JSR-310) +> - Stopwatch +> - Добавил сводку "имя теста - время выполнения" в конце класса + +## Занятие 5: + +### Раскрасил лог (в Spring Boot по умолчанию он тоже colored) +#### Apply 5_4_log_colored.patch +- [Logback layouts coloring](https://logback.qos.ch/manual/layouts.html#coloring) +- Дополнительно: [use colored output only when logging to a real terminal](https://stackoverflow.com/questions/31046748) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Транзакции +- wiki Транзакция +- readOnly и Propagation.SUPPORTS +- Ресурсы: + - How does Spring @Transactional Really Work + - Стратегии работы с транзакциями: Распространенные ошибки + - Spring @Transactional - isolation, propagation + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Профили Maven и Spring +#### Apply 5_5_profiles_connection_pool.patch +> - **Галочка в XML профиле влияет только на отображение в IDEA и никак на выполнение кода.** +> - `Profiles.ACTIVE_DB` задает активный профиль базы (postgres/hsqldb) +> - `Profiles.REPOSITORY_IMPLEMENTATION` определяет реализацию репозитория при запуске приложения (для тестов задаются через `@ActiveProfiles`). + +> Для переключения на HSQLDB необходимо: +> - поменять в окне Maven Projects профиль (Profiles) на `hsqldb` и сделать `Reimport All Maven Projects` (1-я кнопка) +> - поменять `Profiles.ACTIVE_DB = HSQLDB` +> - почистить проект `mvn clean` (фаза `clean` не выполняется автоматически, чтобы каждый раз не перекомпилировать весь проект) + +Для корректного отображения неактивного профиля в IDEA проверьте флаг _Inactive profile highlighting_ и сделайте проекту clean + +![image](https://cloud.githubusercontent.com/assets/13649199/25120020/29935958-2425-11e7-8363-1ff027426f64.png) + +> Вопрос: почему после этого патча не поднимется Spring при запуске приложения в Tomcat? (будем чинить в ДЗ п.6) + +- Using Spring Profiles in XML Config +- Spring Profiles example + +### Автоматический выбор профиля базы: [`ActiveProfilesResolver`](http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver) +#### Apply 5_6_profile_resolver.patch +> Сделал автоматический выбор профиля базы при запуске приложения (тестов) в зависимости от присутствия драйвера базы в classpath (`Profiles.getActiveDbProfile()`) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Пул коннектов +- Выбор реализации пула коннектов: BoneCP, Commons Database Connection Pooling, HikariCP +- Самый быстрый пул соединений на java (читаем комменты) +- Tomcat pool + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Spring Data JPA +#### Apply 5_7_spring_data_jpa.patch +> - Переименовал классы _Proxy_ на более адекватные _Crud_ +> - В `spring-framework-bom` мы уже задали версию Spring. Убрал из остальных зависимостей. +> - В spring-data-jpa 2.x поменялся интерфейс: `T CrudRepository.findOne(ID id)` -> `Optional CrudRepository findById(ID id)` +> - [Java Optional — Отец холиваров](http://sboychenko.ru/java-optional) +> - [Java 8 Optional In Depth](https://www.mkyong.com/java8/java-8-optional-in-depth/) + +- Spring Data JPA +- Замена AbstractDAO: JPA Repositories +- Разрешение зависимостей: Maven BOM [Bill Of Materials] Dependency +- Делегирование (в конце статьи) +- Getting started with Spring Data JPA +- Query methods +- Spring Data – новый взгляд на persistence (JeeConf) +- Евгений Борисов — Spring Data? Да, та! +- Ресурсы: + - Github repositories + - Spring Data JPA Tutorial + - Spring Data JPA with QueryDSL + - [SpEL support in Spring Data JPA @Query](https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions) + +![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы: + +> Зачем мы переопределяем `@Override Meal save` в `CrudUserRepository`. Без этого все работает. + +Можно не переопределять. Сделал только для явного указания всех используемых методов из наследуемого `CrudRepository` + +> Какой паттерн проектирования применён в классе DataJpaUserRepositoryImpl (декоратор/адаптер/другой)?: + +Вопрос интересный:) Ближе всего к адаптеру, но скорее композиция с делегированием. Мы просто используем для нашей реализации возможности `data-jpa: CrudUserRepository`. +Делегат интерфейсов не меняет, а прокси похож на делегата, но служит неявной подмены (часто прямо в рантайм). См. [ПАТТЕРНЫ +ПРОЕКТИРОВАНИЯ](https://refactoring.guru/ru/design-patterns) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Spring кэш +#### Apply 5_8_spring_cache.patch +> - Сделал миграцию на [Ehcache 3.x, compatibile with javax.cache API (JSR-107)](http://www.ehcache.org/) +> - [Spring 4+ with Ehcache 3 – how to](https://imhoratiu.wordpress.com/2017/01/26/spring-4-with-ehcache-3-how-to/) +> - [Новая XML конфигурация](http://www.ehcache.org/documentation/3.4/xml.html) +> - [Supplement JSR-107’s configurations](http://www.ehcache.org/documentation/3.1/107.html#supplement-jsr-107-configurations) +> - В `UserServiceTest.setUp` вместо вызова метода `UserService.evictCache` сделал очистку програмно через `CacheManager` +> - [Evict Ehcache elements programmatically, using Spring](https://stackoverflow.com/questions/29557959/evict-ehcache-elements-programmatically-using-spring) + +- Кеширование в Spring Framework +- Дополнительно: + - Spring cache Abstraction + - Распределённая система кеша ehcache + - Починка JUnit: один кэш на JVM + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> В spring-petclinic `DataJpa` реализована без дополнительных классов. В таком виде как у них, spring data смотрится, конечно, намного лаконичней других реализаций, но у нас получилось вдвое больше кода, чем с тем же jpa или jdbc. Плюс только пожалуй в том, что query находятся прямо в репозитории, а не где-то там в другом пакете. Так что получается, spring data лучше подходит для простейших crud без всяких "фишек"? или в чем его достоинство для больших и сложных проектов? + +Достоинство DATA-JPA по сравнению например с JPA: есть типизация, готовые реализации типовых методов CRUD а также paging, data-common. Мы можем переключить реализацию JPA, например, на mongoDb (`PagingAndSortingRepository`, от которого наследуется `JpaRepository`, находится в `spring-data-common`). +Соответственно его методы будут поддерживаться всеми реализациями `spring-data-common`, JPA одна из них и пр. Подробнее о них есть в видео Spring Data – новый взгляд на persistence. +Дополнительное проксирование в DATA-JPA - моя "фишка" для устранения минусов этого фреймворка: невозможность дебага, привязка к интерфейсу JpaRepository, перенос логики Repository в слой сервисов. +Для большого приложения выигрыш этого стоит. Для небольших (тестовых) приложений дополнительных классов можно не делать. + +> Почему мы для InMemory не сделали отдельного профиля? Почему их не удалить вообще? + +Реализация InMemory является примером как в test делать подмену контекста. Для них сделали отдельный `inmemory.xml` и запускаемый проект ничего не должен о них знать. У нас учебный проект, в котором 4 реализации репозиториев, в реальном такого не будет. + +> А как делать транзакционность для реализации jdbc? + +Будем делать на следующем уроке + +-------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Домашнее задание HW05 + +- 1: Имплементировать `DataJpaMealRepositoryImpl` +- 2: Разделить реализации Repository по профилям Spring: `jdbc`, `jpa`, `datajpa` (общее в профилях можно объединять, например ``). + - 2.1: Профили выбора DB (`postgres/hsqldb`) и реализации репозитория (`jdbc/datajpa/jpa`) независимы друг от друга и при запуске приложения (тестов) нужно задать тот и другой. + - 2.2: Для интеграции с IDEA не забудте выставить в `spring-db.xml` справа вверху в `Change Profiles...` профили, например `datajpa, postgres` + - 2.3: Общие части для всех в `spring-db.xml` можно оставить как есть без профилей вверху файла **(до первого `Java Persistence/OneToMany + +--------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW05 +- 1: Для того, чтобы не запускались родительские классы тестов нужно сделать их `abstract` +- 2: В реализациях `JdbcMealRepository` **код не должен дублироваться**. Если вы возвращаете тип `Object`, посмотрите в сторону дженериков. +- 3: В `MealServlet/SpringMain` в момент `setActiveProfiles` контекст спринга еще не должен быть инициализирован, иначе выставление профиля уже ничего не будет делать. +- 4: Если у метода нет реализации, то стандартно бросается `UnsupportedOperationException`. +- 5: Для уменьшения количества кода при реализации _Optional_ (п. 7, только `DataJpa`) попробуйте сделать `default` метод в интерфейсе +- 6: В Data-Jpa метод для ссылки на entity (аналог `em.getReference`) : `T getOne(ID id)` +- 7: Проверьте, что в `DataJpaMealRepositoryImpl` все обращения к DB выполняются в **одной транзакции** +- 8: Для `достать по id пользователя вместе с его едой` я в `User` добавил `List meals` +- 9: Проверьте, что все тесты запускаются из Maven (имена классов тестов удовлетворяют соглашению) и итоги тестов класса выводятся корректно (не копятся) +- 10: `@ActiveProfiles` принимает в качестве параметра строку, либо **массив** строк. В тестах можно задавать несколько `@ActiveProfiles` в разных классах, они суммируются +- 11: В релизации 7.1 учесть, что у юзера может отсутствовать еда +- 12: [Ordering a join fetched collection in JPA using JPQL/HQL](http://stackoverflow.com/questions/5903774/ordering-a-join-fetched-collection-in-jpa-using-jpql-hql) +- 13: `Topjava + +## Материалы занятия + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW5 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW5: Spring Profiles. Spring Data JPA + +#### Apply 6_01_HW5_data_jpa.patch +> В `get` получаем и фильтруем `Optional` + +Транзакция начинается, когда встречается первый `@Transactional`. С default propagation `REQUIRED` остальные `@Transactional` просто участвуют в первой. Поэтому ставим ее сверху `DataJpaMealRepositoryImpl.save()`, чтобы все обращения к базе внутри метода были в одной транзакции. Анологично, если из сервиса собирается несколько запросов в репозитории, он ставится над методом сервиса. + +#### Apply 6_02_HW5_profile_test.patch +**Для IDEA в `spring-db.xml` не забудте выставить Spring Profiles: например `datajpa, postgres`** + +> - `DbTest` переименован в `AbstractServiceTest` и сюда перенес `@ActiveProfiles(resolver = ActiveDbProfileResolver.class)` +> - Заменил `description.getMethodName()` на `getDisplayName()` в выводе результатов тестов. После `printResult()` буфер сбрасывается в 0, чтобы не накапливать изменения. + +#### Apply 6_03_extract_rules.patch +> Вынес измерение времени и сводку в отдельный класс `TimingRules` + +[JUnit Rules External Resources](https://carlosbecker.com/posts/junit-rules/#external-resources) + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW5: Optional + +#### Apply 6_04_HW5_optional_fix_jdbc_profiles.patch +> Сделал классы `Java8JdbcMealRepositoryImpl` и `TimestampJdbcMealRepositoryImpl` внутренними + +- Spring Profiles. Spring 4 Conditional. +- зайдите в исходники `@Profile` и посмотрите (подебажте) его реализацию через `@Conditional(ProfileCondition.class)`. +- дополнительно: [реализация через Java Config и Profiles на уровне методов](http://stackoverflow.com/a/43645463/548473) + +#### Apply 6_05_update_hsqldb.patch +В реальном проекте часто проблему можно решить простым обновлением версии: new HSQLDB version supports Java 8 time API + +#### Apply 6_06_HW5_optional_fetch_join.patch +> - Добавил проверки и тесты на `NotFound` для `MealService.getWithUser` и `UserService.getWithMeals` +> - Убрал `CascadeType.REMOVE`, в уроке далее будет про Cascade. + +- JPA JoinColumn vs mappedBy +- Unidirectional OneToMany + +#### Apply 6_07_HW5_graph_batch_size.patch +> Сделал `@EntityGraph` [через `attributePaths`](https://spring.io/blog/2015/09/04/what-s-new-in-spring-data-release-gosling) + +- **N+1 selects issue** +- Using Named Entity Graphs + - [JPA ENTITY GRAPHS](http://www.radcortez.com/jpa-entity-graphs/) + - [`EntityGraphType.FETCH` vs `LOAD`](http://stackoverflow.com/questions/31978011/what-is-the-diffenece-between-fetch-and-load-for-entity-graph-of-jpa) +- Стратегии загрузки коллекций в JPA +- Стратегии загрузки коллекций в Hibernate + +> Когда мы достаем всех юзеров с ролями без `@BatchSize` делается запрос юзеров (1) и на каждого юзера идет в базу запрос ролей (+N). +C `@BatchSize(size = 200)` делается запрос на юзеров (1) и затем роли достаются пачками для 200 юзеров (+ N/200). + +## Занятие 6: + +### Добавил тесты на валидацию +> - к сожалению в JUnit нет `ExpectedException.expectRootCause`, а `expectCause` нам не подходит. В 13 JUnit обещают `expectThrows()`, а пока сделал вручную: +`AbstractServiceTest.validateRootCause()` + + > ![](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Откуда у нас берется ConstraintViolationException в тестах на валидацию? Для каких наших исключений он является рутом? + + Прежде всего - пользуйтесь дебагом! Исключение легко увидеть в методе `getRootCause()`. Если подебажить выполение Hibernate валидации, то можно найти, где обрабатываются аннотации валидации и место в `org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate()`, где бросается `ConstraintViolationException`. + +#### Apply 6_08_add_test_validation.patch +**Тесты валидации для Jdbc не работают, нужно будет починить в HW6 (в реализация Jdbc валидация отсутствует)** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Кэш Hibernate +> Кэш мигрировал на 3.x + +#### Apply 6_09_hibernate_cache.patch +**Теперь уже все Jdbc тесты поломались. Требуется починить в HW6** + +- Уровни кэширования Hibernate +- Hibernate Cache. Практика +- Hibernate - Caching +- Починка тестов: инвалидация кэша Hibernate +- [Hibernate User Guide: Caching](http://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#caching) +- [Hibernate 5, Ehcache 3.x](http://www.boraji.com/hibernate-5-query-cache-entity-cache-and-collection-cache-example) +- Ресурсы: + - **Hibernate performance tuning (Mikalai Alimenkou /Igor Dmitriev)** + - JPA2 @Cacheable vs Hibernate @Cache + - How does Hibernate Query Cache work + - Pitfalls of the Hibernate Second-Level / Query Caches + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Cascade. Auto generate DDL. +#### Apply 6_10_cascade_ddl.patch +#### Cascading +> Есть SQL ON .. CASCADE, которая выполняется в базе данных и есть аннотация в Hibernate, исполняемая в приложении + +- Do not use `CascadeType` for @ManyToOne +- CascadeType meaning +- No cascade option on an ElementCollection, the target objects are always persisted, merged, removed with their parent. +- Create ON DELETE CASCADE: `@OnDelete` +- Hibernate second level cache and ON DELETE CASCADE in database schema +- [`orphanRemoval=true` vs `CascadeType.REMOVE`](http://stackoverflow.com/a/19645397/548473) +- [JPA `cascade/orphanRemoval` doesn't work with `NamedQuery`](http://stackoverflow.com/questions/7825484/jpa-delete-where-does-not-delete-children-and-throws-an-exception) + +#### Auto schema generation +- JPA DATABASE SCHEMA GENERATION +- hbm2ddl.auto and autoincrement +- Hibernate/JPA DB Schema Generation Best Practices + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Spring Web +#### Apply 6_11_spring_web.patch +> - Для сборки проекта в окне Maven отключите тесты (`Toggele 'Skip Tests' Mode`) +> - В `web.xml` задаются профили запуска по умолчанию: `postgres,datajpa`. +### Если запускаетесь под HSQLDB, надо поменять на `hsqldb,datajpa`. + +- ServletContextListener. +- Servlet Lifecycle + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. JPS, JSTL, internationalization +#### Apply 6_12_jsp_jstl_i18n.patch +> Поменял `users\meals` в ключах локализации на `user\meal`. Понадобится при локализации ошибок (сделаем позже) + +Переключение автоматического показа ASCII-кодов в IDEA (JSTL локализация кириллицы требует ASCII кодов) + +![image](https://user-images.githubusercontent.com/13649199/38526500-4abe69a8-3c5f-11e8-8849-3956eee024e0.png) + +- Including Content in a JSP Page + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Динамическое изменение профиля при запуске. + + -Dspring.profiles.active="datajpa,postgres" + +- Set profiles in Spring 3.1 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Конфигурирование Tomcat через maven plugin. Jndi-lookup. +С плагином мы можем сконфигурировать Tomcat приямо в `pom.xml` и запустить его с задеплоенным туда нашим приложением WAR из командной строки +без IDEA и без инсталляции Tomcat. По умолчанию он скачивает его из центрального maven репозитория (можно также указать свой в `${container.home}`). +При запуске Tomcat из IDEA запускается Tomcat, путь к которому мы прописали в конфигурации запуска (со своими настройками). + +#### Apply 6_13_tomcat_pool_jndi_cargo.patch +> - для запуска в Tomcat 9 поменял `tomcat7-maven-plugin` на `cargo-maven2-plugin`. +> - плагин сконфигурирован под postgres. Для HSQLDB нужно скорректировать `dependencies` и `driverClassName` в `context.xml` + +> ![](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Томкат сам управляет пулом коннектов ? На каждый запрос в браузере будет даваться свой коннект? + +Да, в томкате есть реализация пула коннектов `tomcat-jdbc` (мы его подключаем со `scope=provided`). Если запускаемся с профильем `tomcat`, приложение на каждую транзакцию (или операцию не в транзакции) берет коннект к базе из пула, сконфигурированного в подкладываемом tomcat `context.xml`. + +Запуск из коммандной строки: + + mvn clean package -DskipTests=true org.codehaus.cargo:cargo-maven2-plugin:1.7.0:run + +Приложение деплоится в application context topjava: [http://localhost:8080/topjava](http://localhost:8080/topjava) + +- Cargo Maven2 plugin +- Катомизация context.xml в cargo-maven2-plugin +- Tomcat JNDI Resources +- BasicDataSource Configuration + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Spring Web MVC +#### Apply 6_14_spring_webmvc.patch +> - Починил [путь к корню](http://stackoverflow.com/questions/10327390/how-should-i-get-root-folder-path-in-jsp-page) +> - В Spring 4.3 ввели новые аннотации `@Get/Post/...Mapping` (сокращенный вариант `@RequestMapping`) + +- Spring Web MVC +- [ContextLoaderListener vs DispatcherServlet](https://howtodoinjava.com/spring-mvc/contextloaderlistener-vs-dispatcherservlet/) +- Паттерн Front Controller +- Иерархия контекстов в Spring Web MVC +- Сценарий обработки запроса. HandlerMappings +- View Resolution: прячем jsp под WEB-INF. +- HandlerMapping: SimpleUrlHandlerMapping, BeanNameUrlHandlerMapping +- Маппинг ресурсов. +- Ресурсы: + - Spring MVC hello world + - Special bean types in the WebApplicationContext + +> Настройки `Project Structure->Modules->Spring`: + +![image](https://cloud.githubusercontent.com/assets/13649199/22221277/52c03cb4-e1c3-11e6-9039-08787e31a505.png) + +> ![](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) В `web.xml` мы инициализируем `DispatcherServlet`, передавая ему параметром `spring-mvc.xml`. Получается, что `DispatcherServlet` парсит `spring-mvc.xml` и находит в нем context? + +Да, можно подебажить родителя `FrameworkServlet.initWebApplicationContext()`. После инициализации сервлет `DispatcherServlet` раскидывает все запросы по контроллерам (бинам контекста спринга). См паттерн Front Controller. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 10. Spring Internationalization +#### Apply 6_15_spring_i18n.patch +- **Spring локализация кириллицы делается в UTF-8 (НЕ требует ASCII кодов)** +- Убедитесь что в настройках IDEA кодировка везде UTF-8 + +> - В локализации поменял `fmt:message` на `spring:message` +> - Выбор языка зависит от языка операционной системы и хедера `Accept-Language`. Добавил в `spring-mvc.xml` `messageSource` параметр [`fallbackToSystemLocale`](http://stackoverflow.com/questions/4281504/spring-local-sensitive-data). +Он управляет выбором, куда переключаться при выборе en и отсутствии `app_en.properties`: локаль операционной системы или `app.properties` (`fallbackToSystemLocale=false`). Переключение локалей будем реализовывать в конце проекта. + +#### Для тестирования локали [можно поменять `Accept-Language`](https://stackoverflow.com/questions/7769061/how-to-add-custom-accept-languages-to-chrome-for-pseudolocalization-testing). Для хрома в `chrome://settings/languages` перетащить нужную локаль наверх. + +- Reloadable MessageSources +- nginx: Serving Static Content + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Кэш hibernate надстраивается над ehcache или он живет самостоятельно? + +- Understanding Hibernate Caching: +Hibernate supports following open-source cache implementations out-of-the-box: EHCache (Easy Hibernate Cache), OSCache (Open Symphony Cache), Swarm Cache, and JBoss Tree Cache. + +> Где конфигурится интернализация для jstl (т.е. файл, где задаются app, app_ru.properties)? Достаточно указать в страницах бандл и путь в ресурсы? + +`` означает что ресурсы будут искаться в `classpath:messages/app(_xx)/properties`: +Tag setBundle: fully-qualified resource name, which has the same form as a fully-qualified class name. +После сборки проекта maven их можно найти в `target/classes` или `target/topjava/WEB-INF/classes`. + +> Отлично, что она все пишет на том языке, который пришел в хидере запроса. А если я хочу выбрать? + +Выбор языка зависит от языка операционной системы и хедера `Accept-Language`. Параметр `fallbackToSystemLocale`, который управляет выбором, когда с `Accept-Language: en,en-US;` не находится локализация `app_en.properties`. Для переключения локали используется JSTL Format Tag fmt:setLocale. Мы будем реализовывать переключение локалей в Spring i18n в конце проекта. + +> Мы создаем бин, где получаем dataSource по имени ``. +Но там не указан класс, как в других dataSource? Получается по имени jdbc/topjava нам уже отдает готовый обьект dataSource и мы как бы помещаем его в бин? + +Здесь используется namespace `jee:jndi-lookup`, который прячет под собой классы реализации. JNDI объект DataSource конфигурируется в `src/main/resources/tomcat/context.xml` + +> В плагине прописан профиль `tomcat,datajpa`, а в web.xml `postgres,datajpa`. +Какой же реально отрабатывает? + +См. видео урока "Динамическое изменение профиля при запуске". В плагине мы задаем параметры JVM запуска Tomcat + +> Почему мы не используем элемент `` в `spring-db.xml`? + +В проекте у нас сейчас 2 Spring контекста: `spring-mvc.xml (см. web.xml, DispatcherServlet)` и родительский `spring-app.xml + spring-db.xml (web.xml, contextConfigLocation)`. +Грубо: 2 мапы, причем для mvc доступно все что есть в родителе. Те `spring-db.xml` не является отдельным самостоятельным контекстом и достаточно того, что `` у нас есть в `spring-app.xml`. + +> A `@NamedQuery` или `@Query` подвержены кешу запросов? Т.е. если мы поставим _USE_QUERY_CACHE_value_="true" будет Hibernate их кешировать? + +Чтобы запрос кэшировался, кроме true в конфигурации нужно еще явно выставить запросу _setCacheable_ (http://vladmihalcea.com/2015/06/08/how-does-hibernate-query-cache-work/). По поводу кэширования `@NamedQuery` нашел `@QueryHint`: https://docs.jboss.org/jbossas/docs/Clustering_Guide/5/html/ch04s02s03.html + +> Почему messages мы кладем в config и используем system environment? разве так делают в реальном проекте? не будешь же вписывать на сервере эти переменные каждый раз, если проект куда-то будет переезжать. Можно по другому, кроме systemEnvironment['TOPJAVA_ROOT'] задать путь от корня проекта? + +1. messages нам нужны в runtime (при работе приложения). Проект к собранному и задеплоенному в Tomcat war отношения никакого уже не имеет и на этом сервере он обычно не находится. Если ресурсы нужны только при сборке и тестировании, то путь к корню для одномодульного maven проекта можно задать как `${project.basedir}`, но для многомодульного проекта (а все реальные проекты многомодульные) это путь к корню своего модуля. +2. В "реальном приложении" делается совершенно по разному: + - нести с собой в classpath, но ресурсы нельзя будет динамически (без передеплоя) обновлять + - класть в war (не в classpath) и обновлять в развернутом TOMCAT_HOME/webapps/[appname]/... + - класть в зафиксированное определенное место (например в home: `~` или в путь от корня `/app/config`). Можно задавать фиксированный пусть в пропертях профиля maven и фильтровать ресурсы (maven resources), чтобы они попали в проперти проекта. + - делать через переменную окружения, как у нас + - задавать в параметрах запуска JVM как системную переменную через -D.. + - располагать в преференсах (для unix это home, для windows- registry): использование Preferences API + - держать настройки в DB + + Часто в одном приложении используют несколько способов для разных видов конфигураций. + +> Не происходит ли дублирования при кэшировании пользователей чрез Hibernate и `@Cacheable` ? + +`@Cacheable` кэширует результат запроса `getAll()`, те список юзеров. Hibernate кэширует юзеров по отдельности, те, грубо мапа, id->User. Те можно назвать это дублированием. Нужно ли будет такое в реальном приложении - все смотрится из логики запросов и их частоты, вполне вероятно что нет. Как то мы писали приложение для Дойчебанка (аналог skype на GWT, те на экране небольшое окошко)- там было 5!! уровней кэширования, первый вообще в базе. + +> У меня стоит томкат 8 версии, в помнике у нас 9 прописан, но всё работает. Почему? + +В `pom.xml` мы подключаем `tomcat-servlet-api` со `scope=provided`, что означает что он используется только для компиляции и не идет в war. Тк мы не используем никаких фич Tomcat 9.x, то наш код совместим с Tomcat 8.x. При запуске через `cargo-maven2-plugin` Tomcat 9 загружается из maven репозитория. + +> Откуда `@Transactional` вытягивает класс для работы с транзакцией, в составе какого бина он идет? + +1. Если в контексте Spring есть ``, то подключается `BeanPostProcessors`, который проксирует классы (и методы), помеченные `@Transactional`. +2. По умолчанию для TransactionManager используется бин с `id=transactionManager` + +--------------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW06 +- 1.1 Починить тесты `InMemoryAdminRestControllerSpringTest/InMemoryAdminRestControllerTest` (в новой версии Spring классы `spring-mvc` требуют `WebApplicationContext`, поэтому поправьте `inmemory.xml`) +- 1.2 Починить Jdbc тесты (валидацию исключить) + - org.junit.Assume + - How to get Current Profiles in Spring Application +- 1.3 Удалить сервлеты и перенести функциональность `MealServlet` в `JspMealController` контроллер (по аналогии с `RootController`). +`MealRestController` у нас останется, с ним будем работать позже. + - 1.3.1 разнести запросы на update/delete/.. по разным методам (попробуйте вообще без `action=`). Можно по аналогии с `RootController#setUser` принимать `HttpServletRequest request` (аннотации на параметры и адаптеры для `LocalDate/Time` мы введем позже). + - 1.3.2 в одном контроллере нельзя использовать другой. Чтобы не дублировать код можно сделать наследование контроллеров от абстрактного класса. + - 1.3.3 добавить локализацию и `jsp:include` в `mealForm.jsp / meals.jsp` + +#### Optional +- 2.1 Добавить транзакционность (`DataSourceTransactionManager`) в Jdbc реализации +- 2.2 Добавить еще одну роль к юзеру Admin (будет 2 роли: `ROLE_USER, ROLE_ADMIN`) +- 2.3 Добавить проверку ролей в UserTestData.assertMatch +- 2.4 Починить имеющиеся и добавить новые тесты в `JdbcUserRepositoryImpl` (добавить роли). Доставать можно двумя способами: одним запросом с JOIN либо двумя запросами: отдельно `users` и отдельно `roles`. + - 2.4.1 В реализации `getAll` НЕ делать запрос ролей для каждого юзера (N+1 select) + - 2.4.2 При save посмотрите на batchUpdate() +- [Объяснение SQL JOIN](http://www.skillz.ru/dev/php/article-Obyasnenie_SQL_obedinenii_JOIN_INNER_OUTER.html) + +--------------------- +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW06 +- 1: Неверная кодировка UTF-8 с Spring обычно решается фильтром `CharacterEncodingFilter`: +``` + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + +``` +- 2: **Если не поднимается контекст Spring, смотрим причину в верху самого нижнего эксепшена.** Все ошибки на отсутствия бина в контексте или его нескольких реализациях относятся к пониманию основ: Spring application context. Если нет понимания этих основ, двигаться дальше нельзя, нужно вернуться к видео Спринг, где объясняется что это такое. Также пересмотрите видео [Тестирование UserService через AssertJ](https://drive.google.com/file/d/1SPMkWMYPvpk9i0TA7ioa-9Sn1EGBtClD). Начиная с 11.30 как раз разбираются подобные ошибки. +- 3: Если неправильно формируется url относительно контекста приложения (например `/topjava/meals/meals`), посмотрите на + - Relative paths in JSP + - Spring redirect: prefix +- 4: При проблемах с запуском томкат проверьте запущенные `java` процессы, нет ли в `TOMCAT_HOME\webapps` приложения каталога `topjava`, логи tomcat - нет ли проблем с доступом к каталогам или контекстом Spring +- 5: Если создаете List с одним значением или Map с одним ключом-значением, пользуйтесь `Collections.singleton..` +- 6: В MealController общую часть `@RequestMapping(value = "/meals")` лучше вынести на уровень класса +- 7: Не забывайте при реализации `JdbcUserRepositoryImpl` про `Map.computeIfAbsent` +- 8: Проверьте `@Transactional(readOnly = true)` сверху `Jdbc..RepositoryImpl` +- 9: Проверьте, что `config\messages\app_ru.properties` у вас в кодировке UTF-8 (в любом редакторе/вьюере или при отключенном [Transparent native-to-ascii conversion](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%9F%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C-%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D1%83-utf-8) в IDEA). ASCII коды нужны были только для JSP +- 10: Учтите, что роли у юзеров можно менять/добавлять/удалять +- 11: Убедитесь, что все методы UserService корректно работают с юзерами, у которых несколько ролей diff --git a/doc/lesson07.md b/doc/lesson07.md new file mode 100755 index 000000000000..8523ee6cf1f3 --- /dev/null +++ b/doc/lesson07.md @@ -0,0 +1,182 @@ +# Онлайн проекта Topjava + +## Материалы занятия + +## ![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 +> - Добавил `AbstractServiceTest.isJpaBased()` и `Assume.assumeTrue(isJpaBased())` в `AbstractMealServiceTest.testValidation()`. +> - Как вариант можно было вместо наследования от `AbstractJpaUserServiceTest` сделать `@Autowired(required = false) JpaUtil` (в этом случае, если в профиле jpa/datajpa не объявить `JpaUtil`, в тестах будет NPE) +> - В новой версии Spring классы `spring-mvc` требуют `WebApplicationContext`, поэтому поправил `inmemory.xml` + + +#### Apply 7_02_HW6_meals.patch +При переходе на 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 +#### Apply 7_04_HW6_optional_add_role.patch +`JdbcUserServiceTest` отвалились. Будем чинить в `7_06_HW6_optional_jdbc.patch` + +#### Apply 7_05_fix_hint_graph.patch +- В `JpaUserRepositoryImpl.getByEmail` DISTINCT попадает в запрос, хотя он там не нужен. Это просто указание Hibernate не дублировать данные. +Для оптимизации можно указать Hibernate делать запрос без distinct: [15.16.2. Using DISTINCT with entity queries](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct) +- Тест `DataJpaUserServiceTest.testGetWithMeals()` не работает для admin (у админа 2 роли и еда при JOIN дублируется). `DISTINCT` при несколький JOIN не помогает. +Оставил в графе только `meals`. Корректно поставить тип `LOAD`, чтобы остальные ассоциации доставались по стратегии модели. Однако [с типом по умолчанию `FETCH` роли также достаются](https://stackoverflow.com/a/46013654/548473) (похоже что бага). + +#### Apply 7_06_HW6_optional_jdbc.patch +> - реализовал в `JdbcUserRepositoryImpl.getAll()` доставание ролей через лямбду +> - в `insertRoles` поменял метод `batchUpdate` и сделал проверку на empty + +Еще интересные JDBC реализации: + - в `getAll()/ get()/ getByEmail()` делать запросы с `LEFT JOIN` и сделать реализацию `ResultSetExtractor` + - подключить зависимость `spring-data-jdbc-core`. Там есть готовый `OneToManyResultSetExtractor`. Можно посмотреть, как он реализован. + - реализация, зависимая от БД (для postgres): доставать агрегированные роли и делать им `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 +``` + +## Занятие 7: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Тестирование Spring MVC +#### Apply 7_07_controller_test.patch +> - в `MockMvc` добавился `CharacterEncodingFilter` +> - добавил `AllActiveProfileResolver` +> - реализация Ehcache нетранзакционная, после отката транзакции в тестах по `@Transactional` Hibernate кэш не восстанавливал роль для USER. Добавил очистку кэша Hibernate в `AbstractControllerTest.setUp` (с учетом того, что `Profiles.REPOSITORY_IMPLEMENTATION` можно переключить на `JDBC`). + +- 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) +#### Apply 7_08_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) +> - [IDEA пока не понимает аннотации `@SpringJUnitConfig`](https://youtrack.jetbrains.com/issue/IDEA-166549). Можно временно поменять на `SpringJUnitConfig + @ExtendWith`. Не забудте залогинится и проголосовать за починку бага (справа вверху). + + +- [JUnit 5 homepage](https://junit.org/junit5) +- [Overview](https://junit.org/junit5/docs/snapshot/user-guide/#overview) +- [10 интересных нововведений](https://habr.com/post/337700) +- Дополнительно: + - [Extension Model](https://junit.org/junit5/docs/current/user-guide/#extensions) + - [A Guide to JUnit 5](http://www.baeldung.com/junit-5) + - [Migrating from JUnit 4](http://www.baeldung.com/junit-5-migration) + - [Before and After Test Execution Callbacks](https://junit.org/junit5/docs/snapshot/user-guide/#extensions-lifecycle-callbacks-before-after-execution) + - [Conditional Test Execution](https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-conditional-execution) + - [Third party Extensions](https://github.com/junit-team/junit5/wiki/Third-party-Extensions) + - [Реализация assertThat](https://stackoverflow.com/questions/43280250) + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. [Принципы REST. REST контроллеры](https://drive.google.com/open?id=1e4ySjV15ZbswqzL29UkRSdGb4lcxXFm1) +#### Apply 7_09_rest_controller.patch + +- Понимание REST +- JSON (JavaScript Object Notation) +- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) +- [10 Best Practices for Better RESTful](http://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/) +- [Best practices for rest nested resources](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources) +- Request mapping +- Дополнительно: + - 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) +#### Apply 7_10_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) + +#### Apply 7_11_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) +#### Apply 7_12_json_assert_tests.patch +- [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_13_tests_refactoring.patch + +### ![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) +#### Apply 7_14_soapui_utf8_converter.patch +- Инструменты тестирования REST: + - SoapUi + - Написание HTTP-запросов с помощью Curl +(для Windows можно использовать Git Bash). Для работы с UTF-8 в Windows 10 нужны пляски с бубном: ["Язык и региональные стандарты" -> "Сопутствующие параметры" -> "Административные языковые параметры" -> "Изменить язык системы" -> галка "Бета-версия:Использовать Юникод (UTF-8) для поддержки языка во всем мире"](https://drive.google.com/open?id=1J1WquTv9wenJQ9ptMymXPYGnrvFzAV-L), перезагрузка. + - Postman + - IDEA: Tools->HTTP Client->... + - [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 + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> При выполнении тестов через MockMvc никаких изменений на базе не видно, почему оно не сохраняет? + +`AbstractControllerTest` аннотируется `@Transactional` - это означает, что тесты идут в транзакции и после каждого теста JUnit делает rollback базы. + +> Что получается в результате выполнения запроса `SELECT DISTINCT(u) FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email`? В чем разница в SQL без `DISTINCT`. + +Запросы SQL можно посмотреть в логах. Те `DISTINCT` в `JPQL` влияет на то, как Hibernate обрабатывает дублирующие записи (с `DISTINCT` их исключает). Результат можно посмотреть в тестах или приложении, поставив брекпойнт. +По поводу `SQL DISTINCT` не стесняйтесь пользоваться google, например [оператор SQL DISTINCT](http://2sql.ru/novosti/sql-distinct/) + +> В чем заключается расширение функциональности hamcrest в нашем тесте, что нам пришлось его отдельно от JUnit прописывать? + +hamcrest-all используется в проверках `RootControllerTest`: `org.hamcrest.Matchers.*` + +> Jackson мы просто подключаем в помнике и спринг будет с ним работать без любых других настроек? + +Да, Spring смотрит в 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 + +> Что означает конструкция в `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.testMeals` для `meals.jsp` + - 1.2 `ResourceControllerTest` для `style.css` (проверить status и ContentType) +- 2: Реализовать `MealRestController` и протестировать его через `MealRestControllerTest` + - 2.1 cледите чтобы url в тестах совпадал с параметрами в методе контроллера. Можно добавить логирование `` для проверки маршрутизации. + - 2.2 в параметрах `getBetween` принимать `LocalDateTime` (конвертировать через @DATETIMEFORMAT WITH JAVA 8 DATE-TIME API), а передавать в тестах в формате `ISO_LOCAL_DATE_TIME` (например `'2011-12-03T10:15:30'`). Вызывать `super.getBetween()` пока без проверки на `null`, используя `toLocalDate()/toLocalTime()` (см. Optional п.3) + +#### 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`) + +**На следующем занятии используется JavaScript/jQuery. Если у вас там пробелы, пройдите его основы** + +--------------------- +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW07 +- 1: Ошибка в тесте _Invalid read array from JSON_ обычно расшифровывается немного ниже: читайте внимательно. +- 2: Jackson и неизменяемые объекты +- 3: Jackson JSON Tutorial +- 4: Если у meal, приходящий в контроллер, поля null, проверьте `@RequestBody` перед параметром (данные приходят в формате JSON) +- 5: При проблемах с собственным форматтером убедитесь, что в конфигурации `Topjava + +## Материалы занятия + +- **Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере (в хроме `Ctrl+F5`)** +- **При удалении файлов не забывайте делать clean: `mvn clean`** + +## ![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 + +#### Apply 8_01_HW07_controller_test.patch +> В `RootControllerTest.testMeals()` сделал проверку через `model().attribute("meals", expectedValue)`. + Сравнение происходит через `MealTo.equals()`, который мы можем переопределить, т.к. он Transfer Object, не является сущностью (Entity). +------------------ +- [Persistent classes implementing equals and hashcode](https://access.redhat.com/documentation/en-us/jboss_enterprise_application_platform/4.3/html/hibernate_reference_guide/persistent_classes-implementing_equals_and_hashcode): переопределять `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 +> - Как и для юзера сериализуем json ответ контроллера и сравниваем через `ResultMatcher`. Для `MealTo` используем в сравнении `isEqualTo`. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW7_Optional +#### Apply 8_03_HW07_formatters.patch +> Перенес форматтеры в подпакет `web`, тк они используются Spring MVC + +#### Apply 8_04_HW07_soapui_curl.patch +> Добавил примеры запросов curl в `config/curl.md` + + - Написание HTTP-запросов с помощью Curl (для Windows можно использовать Git Bash) + +## Занятие 8: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. WebJars. jQuery and JavaScript frameworks +#### Apply 8_05_webjars.patch +> - Обновил jQuery до 3.x, Bootstrap до 4.x +> - Новое в jQuery 3 +> - УБРАЛ из проекта Dandelion обертку к datatables: +> - не встречал нигде, кроме Spring Pet Clinic; +> - поддержка работы с datatables через Dandelion оказалось гораздо более трудоемкое, чем работа с плагином напрямую. +> - Исключил из зависимостей webjars ненужные jQuery + +- Подключение веб ресурсов. 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) 4. Bootstrap +#### Apply 8_06_bootstrap4.patch +> - [Мигрировали на Bootstrap 4](https://getbootstrap.com/docs/4.1/migration/) +> - Добавил Font Awesome +> - [Map glyphicon icons to font-awesome](https://gist.github.com/blowsie/15f8fe303383e361958bd53ecb7294f9) +> - В таблице удаление/редактирование сделал без кнопок (линками) + +- [Bootstrap](https://getbootstrap.com/) + - [Bootstrap Examples](https://getbootstrap.com/docs/4.1/examples/) + - [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/) +- Дополнительно + - Twitter Bootstrap Tutorial + - Видео уроки Bootstrap 4 + - [Bootstrap верстка современного сайта за 45 минут](https://www.youtube.com/watch?v=46q2eB7xvXA) + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. AJAX. Datatables. jQuery + JSP полезны, если надо с сервера отдать статический html с серверной логикой (условия, циклы), сформированный на основе модели. Для динамической отрисовки таблицы мы будем использовать REST и JSON на 9м уроке (работа с datatables через Ajax). + +#### Apply 8_07_ajax_datatables.patch +> - Переименовал js скрипты по [javascript filename naming convention](https://stackoverflow.com/questions/7273316/what-is-the-javascript-filename-naming-convention) +> - `reset()` не чистит скрытые (hidden) поля формы. Сделал очистку полей через `form.find(":input").val("")` +> - Поменял форматирование модального окна: [Botstrap4 Modal](https://getbootstrap.com/docs/4.1/components/modal/) + +JavaScript +- AJAX +- Введение в браузерные события +- [Внешние скрипты, порядок исполнения](https://learn.javascript.ru/external-script) +- JavaScript loading modern approach + +jQuery +- Событие $(document).ready. +- jQuery для всех. +- jQuery для начинающих. AJAX. +- jQuery для начинающих. Селекторы. +- [jQuery task from freecodecamp](https://www.freecodecamp.org/map-aside#nested-collapsejQuery) +- jQuery API + +DataTables/Bootstrap +- DataTables API +- Javascript плагины для Bootstrap +- DataTables/Bootstrap 4 integration +- Converting parameter names for 1.10 +- dataTable() vs. DataTable() + +#### Apply 8_08_refactor_js.patch +> - [Заменил `var` на `let`/`const`](https://learn.javascript.ru/let-const)(в IDEA нужно поменять версию JavaScript через `Alt+Enter`). Синтакс поддерживается [95% браузеров](https://caniuse.com/#feat=const). На каждом конкретном проекте поддерживаемые версии браузеров определяются бизнесом. +> - Вместо глабальных переменных `ajaxUrl`, `datatableApi` задаю их в объекте контекст, который передаю в `makeEditable()` как параметр +> - Вынес переменную `form = $('#detailsForm')` (инициализирую только 1н раз) +> - [В `jquery.ajax` заменил depricated `success` на `done()`](http://api.jquery.com/jquery.ajax/#jqXHR) +> - При удалении строки добавил [подтверждение confirm](https://stackoverflow.com/questions/10462839/how-to-display-a-confirmation-dialog-when-clicking-an-a-link) + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Notifications +#### Apply 8_09_notification.patch +> - Сделал [защиту от кэширование ajax запросов в IE](https://stackoverflow.com/a/4303862/548473) +> - Обновил API Noty (3.x), добавил в сообщения font-awesome +> - [Tomcat 8.5.x перестал отдавать в заголовке `statusText`](http://tomcat.apache.org/tomcat-8.5-doc/changelog.html). Отображаем просто `status` +> - RFC 7230 states that clients should ignore reason phrases in HTTP/1.1 response messages. +> - Since the reason phrase is optional, Tomcat no longer sends it (statusText). + +- Обработка ajaxError. +- jQuery notification + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Добавление Spring Security +> - Правка к видео: путь в intercept-url должен быть полный: `pattern="/rest/admin/**"` +> - В Spring Security 4.x по умолчанию включен csrf (защита от межсайтовой подделки запроса). Выключил, включим на 10-м занятии. +> - В Spring Security 5.x по умолчанию пароль кодируется. Выключил, включим на 10-м занятии. +> - [Adding a Password Encoder](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#ns-password-encoder) + +#### Apply 8_10_add_security.patch +- Spring Security +- Протокол AAA +- Методы аутентификации. +- Basic access authentication +- Использование ThreadLocal +- Security with Spring +- [Decode/Encode Base64 online](http://decodebase64.com/) + +`curl -v -H 'Authorization: Basic dXNlckB5YW5kZXgucnU6cGFzc3dvcmQ=' http://localhost:8080/topjava/rest/profile/meals` + +аналогична + +`curl -v --user user@yandex.ru:password http://localhost:8080/topjava/rest/profile/meals` + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Что делает код? +``` +$('.delete').click(function () { + deleteRow($(this).attr("id")); + }); +``` + +На все элементы DOM с классом `delete` вешается обработчик события `click` который вызывает функцию `deleteRow`. Классы в html разделяются через пробел. См. [селекторы в jQuery](http://anton.shevchuk.name/javascript/jquery-for-beginners-selectors/) + +> Как в таблицу `` из JSP вставляются дополнительные `div`, поле для поиска, стрелочки для сортировки и т.д. (видны в браузере через `Inspect (Ctrl+Shift+I)` в хроме) ? + +JSP отдает html на клиента, в браузере играется скрипт `$("#datatable").DataTable(..)`, который модифицирует элемент таблицы и вставляет туда (в элементы DOM html документа) все табличные плюшки. + +> тянет ли Bootstrap за собой jQuery? + +Bootstrap css это стили (форматирование), Bootstrap js зависит от jQuery: http://stackoverflow.com/questions/14608681/can-i-use-twitter-bootstrap-without-jquery#answer-14608772 + +> А где реально этот путь "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 в ware, откуда spring знает где искать наш jquery ? + +В war в `WEB-INF/lib/*` лежат все jar, которые попадают к classpath. Spring при обращении по url `/webjars/` ищет по пути биндинга `` +по всему classpath (то же самое как распаковать все jar в один каталог) в `META-INF/resources/webjars/`. В этом месте во всех jar, которые мы подключили из webjars лежат наши ресурсы. + +> Как можно в браузере сбросить введенный пароль базовой авторизации? + +Проще всего делать новый запрос в новой анонимной вкладке (`Ctrl+Shift+N` в Chrome) + +> Оптимально ли делать доступ к статическим ресурсам (css, js, html) через webjars ? + +На продакшене под нагрузкой статические ресурсы лучше всего держать не в war, а снаружи. Доступ к ним делается либо через конфигурирование Tomcat, но чаще всего через прокси, например Nginx + +> Как по REST определяется залогиненный юзер? Аутентификация происходит при каждом запросе? + +Способы RESTful Authentication. +Мы будем использовать 2: coockie + http session (на след. уроке) и Basic Authentication с аутентификацией при каждом запросе. + +> Почему `@RequestParam` не работает в PUT и DELETE запросах? + +По спецификации Servlet API параметры в теле для PUT, DELETE, TRACE методах не обрабатываются (только в url). +Те. можно: + - использовать POST + - передавать параметры в url + - использовать `HttpPutFormContentFilter` фильтр + - настроить Tomcat в обход спецификации. + +См. Handle request parameters for an HTTP PUT method + +> Данные между браузером и ajax гоняются в виде json? Почему в `AdminAjaxController` у методов delete и createOrUpdate нет в аннотациях параметра `consumes = MediaType.APPLICATION_JSON_VALUE` ? + +Посмотреть на данные между приложением и браузером можно (и нужно!) в браузере (вкладка Network в Инструментах разработчика, F12 в Хроме). Зависит от того, как их отправляем из браузера и из приложения. Данные формы обычно передаются просто параметрами. `APPLICATION_JSON_VALUE` в контроллере нужно, только если параметры отдаются/принимаются в формате JSON. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW08 + +- 1: Перевести `meals` на `datatables` (`meals.jsp`, `MealAjaxController`). + - 1.1 Реализовать добавление записи еды через модальное окно Bootstrap и удаление еды по ajax (БЕЗ редактирования). + - 1.2 При вставке данных по AJAX пропадает все JSP форматирование, чинить перерисовку НЕ надо. Следующий урок- будем делать datatable по AJAX и форматирование на стороне клиента. +- 2: Т.к. HTML атрибут id у каждого элемента документа должен быть уникален, нужно избавиться от дублирования `id="${user.id}"` в строках таблиц users (`users.jsp`) (переместить атрибут id в тэг `` или передавать в качестве параметра функций через `onclick`) + +#### Optional. +- 3: Перевести работу фильтра на AJAX. Попробуйте после модификации таблицы (например добавлении записи) обновлять ее также с учетом фильтра. + - [Руководство по выбору между GET и POST](https://handynotes.ru/2009/08/get-versus-pos.html) +- 4: Сделать кнопку сброса фильтра. +- 5: Реализовать enable/disable User через checkbox в `users.jsp` с сохранением в DB. +Неактивных пользователей выделить css стилем. Проверьте, как у вас первоначально (или по F5) отображаются неактивные пользователи (если меняете css при enable/disable) + + +--------------------- +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW08 +- 1: enable/disable делать c `@Transactional` (можно реализовать как на уровне репозитория, так и на уровне сервиса через несколько sql, которые должны быть в одной транзакции) +- 2: в `topjava.common.js` следует выносите только общие скрипты (cкрипты еды размещайте в `topjava.meals.js`, пользователей в `topjava.users.js`) +- 3: если в контроллер приходит `null` проверте в `Network` вкладке браузера в каком формате приходят данные и в каком формате в контроллере вы их принимаете (`consumes`). +- 4: при реализации `enable/disable` лучше явно указывать нужное состояние, чем переключать на противоположное. Если параллельно вам кто-то изменит состояние, то будет несоответствие UI и DB. Не забудьте про `enable/disable` тесты. diff --git a/doc/lesson09.md b/doc/lesson09.md new file mode 100755 index 000000000000..2e61cfa5dfdd --- /dev/null +++ b/doc/lesson09.md @@ -0,0 +1,180 @@ +# Онлайн проекта Topjava + +## Материалы занятия + +- **Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере по Ctrl+F5** +- **При удалении файлов не забывайте делать clean: `mvn clean package`** + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW8 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW8 +**Поправка к видео: [в гриде bootstrap 12 колонок](https://getbootstrap.com/docs/4.1/layout/grid/)** + +#### Apply 9_01_HW8.patch +> - Все события сделал через `onlick` +> - Фильтр еды сделал в [Bootstrap 4 Cards](https://getbootstrap.com/docs/4.1/components/card/) +> - Удалил лишние классы, JSP и i18N + +- [Bootstrap forms](https://getbootstrap.com/docs/4.1/components/forms/) +- [Grid system](https://getbootstrap.com/docs/4.1/layout/grid/) +- [Difference among col-* in Bootstrap](https://stackoverflow.com/a/19865627/548473) + +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: +> Можно ли было удаление делать без перезагрузки таблицы (удалением строки) и для редактирования брать данные со страницы, а не с сервера? + +В многопользовательском приложении принято при изменении данных подтягивать все изменения с базы, иначе может быть несогласованность базы и UI (например когда пользователей редактируют несколько администраторов одновременно). В таблице еды наши пользователи видят только свои записи, но лучше для всех таблиц делать общий подход. Дополнительная нагрузка на базу тут совсем небольшая. + +#### Apply 9_02_HW8_clear_filter.patch +> Добавил сброс фильтра + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW8 Optional (enable/disable user) + +#### Apply 9_03_HW8_enable_disable.patch +> - Перенес метод `enable` в `topjava.users.js` и сделал стиль для пользователей через атрибут `data-userEnabled` +> - Меняю стиль `` ПОСЛЕ успешной обработки запроса через `data-userEnabled`, при ошибке возвращаю `checked` в прежнее состояние +> - Убрал `init()`. При переводе таблицы на Ajax вместо него будет `createdRow`. Атрибуты `data-userEnabled` добавляются при отрисовки таблицы в JSP + +## Занятие 9: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Spring Binding +#### Apply 9_04_binding.patch + +> Move `ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY` to `ru.javawebinar.topjava.util.UserUtil` + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Реализация update +#### Apply 9_05_update.patch +> - Сделал интерфейс `HasId` от которого наследуются `BaseTo` и `AbstractBaseEntity` +> - Сделал проверку `id` в `ValidationUtil` на основе `HasId` +> - Сделал в `ProfileRestController` обновление своего профиля через `UserTo` (нельзя изменять себе роли) и поправил тест + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Spring Validation +#### Apply 9_06_validation.patch +> - `responseJSON` не выводится в случае его отсутствия (например при попытке добавить пользователья с дублирующимся email) +> - сделал конкатенацию ошибок через `StringJoiner` +> - при неверном формате email делается проверка `startsWith`, чтобы поле email не дублировалось в сообщении + +- Spring Validation. +- Bean Validation +- Валидация формы по AJAX. +- JSR-303, 349 +- @Valid @RequestBody + Error handling +- [Java Bean Validation Basics](https://www.baeldung.com/javax-validation) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Перевод DataTables на Ajax +#### Apply 9_07_datatable_via_ajax.patch +> - Перешли на [параметры Datatables в формате 1.10](https://datatables.net/upgrade/1.10-convert) +> - В `makeEditable()` больше нет манипуляций c DOM, которые требуются делать ПОСЛЕ отработки плагина `datatable`, поэтому нам не обязательно вызывать ее в коллбэке `initComplete`. + +- [DataTables Ajax](https://datatables.net/manual/ajax) + +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: +> Что за дополнительный параметр (который каждый раз инкрементируется) появляется при запросе datatables данных по ajax (например `http://localhost:8080/topjava/ajax/admin/users/?_=1496156621129`) ? + +Это защита `datatables` от кэширования запроса браузером (например в IE). + +#### Apply 9_08_js_i18n.patch +> - Добавил [простую интернационализацию в JavaScript](https://stackoverflow.com/questions/6218970/resolving-springmessages-in-javascript-for-i18n-internationalization). +> - на стороне сервера формируется `i18n` JavaScript массив с значениями, который затем используется для интернационализации в браузере +> - в модальном окне заголовок подменяется через `$('#modalTitle').html(..title)` + +#### Для тестирования локали [можно поменять `Accept-Language`](https://stackoverflow.com/questions/7769061/how-to-add-custom-accept-languages-to-chrome-for-pseudolocalization-testing). Для хрома в `chrome://settings/languages` перетащить нужную локаль наверх. + +- JavaScript internationalization + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Форма логина / логаут. +#### Apply 9_09_min_form_login.patch + +> Добавил функциональность logout + +- Минимальный form-login +- Migrating <form-login> + +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Мои вопросы: +- Почему при логине как admin еда отдаются для user? +- Почему при логине как user не отображается список пользователей? +- Почему еда не редактируется? + +> Подсказка: поглядите вкладку Network в браузере. + +#### Apply 9_10_jsp_form_login.patch +> Рефакторинг +> - В `login.jsp` вместо атрибутов достаю параметры запроса (`param.error/message`). +> - При нажании кнопок `Зайти как ...` сделал вход в приложение + +- Собственный form-login + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Реализация собственного провайдера авторизации. +#### Apply 9_11_auth_via_user_service.patch + +- UserDetailsService Implementations + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Принцип работы Spring Security. Проксирование. +- Технический обзор Spring Security +- Типы проксирования +- Dynamic Proxy API +- Security фильтры + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 10. Spring Security Test +#### Apply 9_12_spring_security_test.patch + +> - Cделал "честную" авторизацию в `RootControllerTest` (через `authentication` в утильном методе `TestUtil`) +> - Cделал `mockAuthorize` для `SpringMain`, в который не попадают фильтры + +- Spring Security Test +- Интеграция со Spring MVC Test +- HttpBasic авторизация +- Тестирование контроллеров с помощью MockMvc (без spring-security-test) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 11. Cookie. Session. +- HTTP cookie +- Under what conditions is a JSESSIONID created? +- Tomcat Session Serialization + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> В куки попадает обычная строка JSESSIONID. Куда сериалиуется объект User? + +Для хранения сосотяния сессии (например корзины покупателя) в Servlet API есть механизм хранения объектов сессии (грубо- мультимапмапа, которая достается из хранилища по ключу). При создании сессии на стороне сервера (через `request.getSession`) создается кука `JSESSIONID`, которая передается между клиентом и сервером и является ключом в хранилище объектов сессий. См. обработка сессий с помощью сервлетов + +> В `login.jsp` есть форма `` Где такой url используется? + +Это стандартный url для авторизации в `spring-security`. Он его и обрабатывает. + +> Если не пользовать js, а писать UI на JSP, сообщения между ui и сервером будут в формате json? Это же будет JSON API? + +Есть данные, которые передаются между клиентом и сервером в формате json или get/post с параметрами, есть стили взаимодействия клиента и сервера (REST, JSON API, JSON-RPC) и есть отрисовка UI: JSP, Javascript фреймворк, Thymleaf и пр. Не надо эти вещи путать между собой. + +> По умолчанию спринг работает с `UserDetailsService`, который должен возвращать `UserDetails`. Но мы не хотим стандартные, мы хотим свои, поэтому просто наследуем наши `UserServiceImpl` и `AuthorizedUser` от соответствующих интерфейсов и реализуем недостающие методы, которые spring security и будет использовать? + +Да. Сервис аутентификации конфигурится в `spring-security.xml` `` и должен реализовывать интерфейс `UserDetailsService`. В spring-security есть его стандартные реализации, которые использовались до нашей кастомной `UserServiceImpl`, например `jdbc-user-service` использует реализацию `JdbcUserDetailsManager` + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW9 + +- 1: Реализовать для meal Binding/ Update/ Validation. Проверить работу при пустом значении `calories`. +- 2: Перевести `meals.jsp` на работу по ajax. Стиль строки таблицы сделать в зависимости от `excess`, время отображать без `T`. Добавить i18n. +- 3: Починить meals тесты, добавить тест на неавторизованный доступ + +#### Optional +- 4: Подключить datetime-picker к фильтрам и модальному окну добавления/редактирования еды + - DateTimePicker jQuery plugin + - [jQuery: конверторы](https://jquery-docs.ru/jQuery.ajax/#using-converters) + +- Попробуйте при запросах по REST оставить стандартный ISO формат (с разделителем `T`). То есть: + - Отображение и редактирование еды на UI происходит без `T` (формат значений на UI можно увидеть во вкладке браузера Network) + - Когда мы работаем по REST, в json и запросах формат даты ISO (с разделителем `T`) + - Напомню, что параметры методов контроллера (даже собранные в объекты через Binding) парсятся конверторами спринга (`@DateTimeFormat`), а объекты json парсится Jackson и они никак не влияют друг на друга. + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Проверка в HW09 +- 1: Проверьте, что при добавлении и редактировании пользователя и еды у вас корректно отображаются заголовки модального окна: +"Добавить/Редактировать еду пользователя" +- 2: Не дублируйте +``` +Topjava + +## [Материалы занятия](https://drive.google.com/open?id=0B9Ye2auQ_NsFfk43cG91Yk9pM3JxUHVhNFVVdHlxSlJtZm5oY3A4YXRtNk1KWEZxRlFNeW8) + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правка + +#### Apply 10_0_fix.patch +- [name/value в MVC аннотациях не нужны](https://habr.com/ru/post/440214/) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW9 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW9 +#### Apply 10_1_HW9_binding_ajax.patch + +Datatables перевели на ajax (`"ajax": {"url": ajaxUrl, ..`), те при отрисовке таблица сама по этому url запрашивает данные. Поэтому в методе `RootController.meals()` нам нужно только возвратить view "meals" (`meals.jsp`) которому уже не нужны данные в атрибутах. + +> - JavaScript `i18n[]` локализацию перенес в `i18n.jsp` и передаю туда `page` как параметр +> - [JSP include action with parameter example](https://beginnersbook.com/2013/12/jsp-include-with-parameter-example) +> - Вынес общий код в `ValidationUtil.getErrorResponse()` +> - Вынес создание `DataTable` в `topjava.common.js`. В параметр конфигурации добавляю общие опции используя [jQuery.extend()](https://api.jquery.com/jquery.extend/#jQuery-extend-deep-target-object1-objectN) + +#### Apply 10_2_HW9_test.patch +#### Apply 10_3_HW9_datetimepicker.patch +> - Изменил формат ввода dateTime в форме без 'T': при биндинге значений к полям формы в `datatablesUtil.updateRow()` для поля `dateTime` делаю `replace('T', ' ')`. REST интерфейс по прежнему работает в стандарте ISO-8601 +> - Вынес общий код в `datatablesUtil.formatDate()` +> - В новой версии `datetimepicker` работает ограничение выбора времени `startTime/endTime` + +- DateTimePicker jQuery plugin +- Datetimepicker and ISO-8601 + +## Занятие 10: + +#### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. [Дополнительно: кастомизация JSON (@JsonView) и валидации (groups)](https://drive.google.com/file/d/0B9Ye2auQ_NsFRTFsTjVHR2dXczA) +**ВНИМАНИЕ! Патчи `10_04_opt` и `10_05_opt` - не обязательные! Если будете делать- то в отдельной ветке (у меня `opt_view_groups`). Это варианты решений, которые не идут в `master`.** +- [Что возвращать: Entity или DTOs](https://stackoverflow.com/a/21569720/548473) + +#### Apply 10_04_opt_json_view.patch +- [Jackson Annotation ](http://www.baeldung.com/jackson-annotations) + - [Jackson JSON Views](http://www.baeldung.com/jackson-json-view-annotation) +- [@JsonView: фильтруем JSON](https://habrahabr.ru/post/307392/) +- [Jackson set default view](https://stackoverflow.com/questions/22875642/jackson-set-default-view) + +#### Apply 10_05_opt_validated_groups.patch +- [Validation Group in SpringMVC](https://narmo7.wordpress.com/2014/04/26/how-to-set-up-validation-group-in-springmvc/) +- [@Validated and Default group](http://web.archive.org/web/20170826153901/http://forum.spring.io/forum/spring-projects/web/117289-validated-s-given-groups-should-consider-default-group-or-not) + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. [Рефакторинг: jQuery конверторы и группы валидации по умолчанию](https://drive.google.com/file/d/1tOMOdmaP5OQ7iynwC77bdXSs-13Ommax) +**ВНИМАНИЕ! далее патчи идут после `10_03_HW9_datetimepicker` в ветке `master`** +#### Apply 10_04_jquery_converters.patch +- [jQuery: типы данных](https://jquery-docs.ru/jQuery.ajax/#data-types) +- [jQuery: конверторы](https://jquery-docs.ru/jQuery.ajax/#using-converters) + +#### Apply 10_05_persist_validate_group.patch +- [Default Hibernate validation](https://stackoverflow.com/a/16930663/548473). Т.к. `Persist` наследуется от `javax.validation.groups.Default`, при сохранении учитываются все непомеченные аннотации валидации (`Default`) + помеченные `Persist`. + +> ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) +При `NotNull(groups = View.Persist.class)` валидация происходит непосредственно перед созданием или редактированием на уровне репозиториев? А если бы мы установили без группы то проверка была сразу после получения с UI? + +Если `View` никаких нет (что равнозначно `javax.validation.groups.Default`), то бины, в которых есть аннотации валидации, проверяют Spring в контроллерах (если перед параметром стоит `@Valid`) и Hibernate перед сохранением и обновлением. Через `View` мы можем управлять валидацией. В нашем случае мы включили в `spring-db.xml` указание Hibernate валидировать поля с `View.Persist`. Так как `View.Persist` наследуется от `Default`, то Hibernate будет также валидировать и поля, на которых нет View. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Spring Security Taglib. Method Security Expressions. +#### Apply 10_06_secure_tag_annotation.patch +> - Сделал общий `bodyHeader.jsp` с разделением `isAnonymous/isAuthenticated` +> - В `login.jsp` кнопки входа отображаю только для `isAnonymous` + +- Spring Security Taglib. +- Method Security и Method Security Expressions. + +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: +> У нас в `sprng-mvc` подключается `spring-app`, в который подключается `spring-security`. Т.о. `spring-mvc` в себе содержит код `spring-app` и `spring-security`. Почему тогда аннотация `Interceptors. Редактирование профиля. JSP tag files. +#### Apply 10_07_interceptor.patch +- Spring interceptors. + +#### Apply 10_08_profile_jsptag.patch +> - Упростил: в `inputField.tag` передаю для label код локализации +> - В профиль добавил кнопку "Cancel" + +- [Bootstrap 4 form validation example](http://nicesnippets.com/snippet/bootstrap-4-form-validation-with-form-all-input-example) +- Делаем jsp tag для ввода поля формы. +- Spring MVC Form Validation +- Spring MVC Form: check if a field has an error + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Форма регистрации. +#### Apply 10_09_registration.patch + +> - Добавил локализацию +> - При регистрации передаю `username` в `login.jsp` как параметр и делаю `setCredentials` (пароль вводится скрыто, поэтому его не передаю из соображений безопасности). Т.к `setCredentials` требует jQuery, убрал для него `defer` +> - Добавил регистрацию пользователя по REST: `ProfileRestController.register` и тест + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Обработка исключений в Spring. + +#### Apply 10_10_not_found_422.patch +> [Поменял код 404 (URL not found) на 422 (Unprocessable Entity)](http://stackoverflow.com/a/22358422/548473) + +- Используем HTTP status code + +#### Apply 10_11_global_exception.patch +> - Перед отображением exception предварительно делаю `ValidationUtil.getRootCause` +> - Добавил локализацию + +- Global Exception Handling + +#### Apply 10_12_controller_advice_exception.patch +**Отображение валидации для формы еды и юзера пропало (`TODO` в коде для HW10). Можно посмотреть сериализацию в `ErrorInfo` при попытке добавить юзера с дублирующимся email.** + +> - Добавил в `ErrorInfo` тип ошибки `ErrorType` +> - Добавил обработку неверного запроса (`IllegalRequestDataException`) +> - `@ResponseBody` над методами `ExceptionInfoHandler` заменил на `@RestControllerAdvice` +> - В `ExceptionInfoHandler` удалил `@Order` над методами и добавил над классом: +``` + Methods are matched by closest exception in + * @see org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#getMappedMethod + * 164: Collections.sort(matches, new ExceptionDepthComparator(exceptionType)) +``` +> - В `ExceptionInfoHandler.logAndGetErrorInfo()` также использую `ValidationUtil.getRootCause` +> - Добавил в `curl.md` пример с возвращением `ErrorInfo`. Локализация ошибок будет на последнем занятии + +- Сериализация Exception в JSON +- Exception Handling на уровне контроллера +- @ControllerAdvice improvements in Spring 4 +- @Valid @RequestBody + Error handling + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Encoding password. Json READ/WRITE access +#### Apply 10_13_password_encoding.patch + +- Password Encoding + +#### Apply 10_14_read_write_access.patch +В реальном приложении для управления паролем необходим отдельный UI интерфейс с подтверждением старого пароля. + +- [@JsonProperty READ_ONLY / WRITE_ONLY](https://stackoverflow.com/a/12505165/548473) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Межсайтовая подделка запроса (CSRF). +#### Apply 10_15_csrf.patch +> Убрал `form:form` из ajax запросов: там csrf работает через header. Проверьте в вкладке браузера Network. + +**Поломалась UTF-8 кодировка в редактировании профиля и регистрациию (если по умолчанию не UTF-8). В Optional HW10 нужно будет починить.** + +- Межсайтовая подделка запроса (CSRF) +- Using Spring Security CSRF Protection +- Ajax and JSON Requests +- Stateless CSRF protection +- Ресурсы: + - Spring Security 4 + CSRF + - Are JSON web services vulnerable to CSRF attacks + - Правило ограничения домена (SOP) + - Cross-origin resource sharing (CORS) + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> В чем отличие между аннотоацией `@PreAuthorize("hasRole('ROLE_ADMIN')")` и конфигурацией в jsp: ``, `` ? + +Анотация `@PreAuthorize` обрабатывается Spring анологично `@Transactional`, `@Cacheable` - класс проксируется и до-после вызова метода добавляется функциональность. +В данном случае перед вызовом метода проверяются роль залогиненного юзера. JSTL тэг `authorize` выполняет проверку условия в залогиненном юзере внутри jsp. + +> Еще раз: почему не нужен csrf для REST и нельзя подделать JSON запрос с вредоносного сайта? + +Попробуйте выполнить ajax запрос из вашего приложения c url, у которого домен отличный от вашего (например 'http://topjava.herokuapp.com/meals/ajax/admin/users/{id}'). +В консоли браузера будет `XMLHttpRequest cannot load`... - нарушение same origin policy. +Формам же разрешается делать submit (через `action=..`) на другой домен, но невозможно cделать `Content-Type`, отличный от стндартных enctype и методов кроме get и post. Таким образом `consumes = MediaType.APPLICATION_JSON_VALUE` в POST защищает приложение от CSRF. + +> Почему использован `BCryptPasswordEncoder`а не `hash(password+salt)`? + +[`BCryptPasswordEncoder` automatically generates a salt and concatenates it with the hash value in a single String](http://stackoverflow.com/a/8528804/548473). + +> Когда запускается в `GlobalControllerExceptionHandler` метод `defaultErrorHandler`? Когда как в него исключение попадает? Как выбирается, кто обрабатывает исключения: `ExceptionInfoHandler` или `GlobalControllerExceptionHandler`? + + `GlobalControllerExceptionHandler` попадает в контекст спринг (через `@ControllerAdvice` его находят в пакете `web`). Далее спринг перехватывает исключения и отправляет в подходящий по исключению метод `GlobalControllerExceptionHandler`. `ExceptionInfoHandler` помечен `@ControllerAdvice(annotations = RestController.class)`, он обрабатывает только ошибки из всех контроллеров с аннотацией `RestController`. + + > Откуда берутся в валидации сообщения на русском "должно быть между 10 и 10000"? + + Локализация встроена в Hibernate Validation. Смотрите `Ctrl+Shift+N` и `ValidationMessages_ru.properties`. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW10 +- 1: Сделать валидацию в AdminAjaxController/MealAjaxController через `ExceptionInfoHandler`. Вернуть клиенту `ErrorInfo` и статус `HttpStatus.UNPROCESSABLE_ENTITY` (тип методов контроллеров вернуть обратно на `void`). +- 2: Сделать валидацию принимаемых json объектов в REST контроллерах через `ExceptionInfoHandler`. Добавить в Rest контроллеры тест для невалидных данных. + - @Valid @RequestBody + Error handling +- 3: Сделать обработку ошибки при дублирования email ("User with this email already exists") для: + - 3.1 регистрации / редактирования профиля пользователя + - Spring MVC form handling example + - 3.2 добавления / редактирования пользователя в таблице + - 3.3 REST контроллеров + +#### Optional +- 4: Сделать обработку ошибки при дублирования dateTime еды. Сделать тесты на дублирование email и dateTime. + - [Тесты на DB exception c @Transactional](http://stackoverflow.com/questions/37406714) + - [Сheck String in response body with mockMvc](https://stackoverflow.com/questions/18336277/how-to-check-string-in-response-body-with-mockmvc) +- 5: Сделать в приложении выбор локали (см. http://topjava.herokuapp.com/) + - Spring MVC internationalization sample + - Spring 4 MVC Internationalization +- 6: Починить UTF-8 в редактировании профиля и регистрации (если кодировка по умолчанию у вас не UTF-8). Подумайте, почему кодировка поломалась. + +------- + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Проверка в HW10 +- 1: `ErrorInfo` просто бин для передачи информации на клиента. Кода возврата и ответ настраиваются в `ExceptionInfoHandler`. +- 2: Не дублируйте обработку ошибок `BindingResult`: `result.getFieldErrors()..` +- 3: Можно не создавать собственные эксепшены, а в `ExceptionInfoHandler` ловить стандартные +- 4: в `MethodArgumentNotValidException` также есть `e.getBindingResult()`, его можно обрабатывать по аналогии с `BindException` +- 5: Не дублируйте код переключения локали на странице логина и в приложении +- 6: При проблемах с валидацией `Meals` в `MealRestController`, посмотрите на валидацию в `MealAjaxController.updateOrCreate` diff --git a/doc/lesson11.md b/doc/lesson11.md new file mode 100755 index 000000000000..acf638349aa9 --- /dev/null +++ b/doc/lesson11.md @@ -0,0 +1,209 @@ +# Онлайн проекта Topjava + +### Обновил Демо приложение нашей версией + +## Материалы занятия +#### Некоторые темы урока новые, поэтому видео есть не везде. +## Все материалы проекта (в том числе и будущие обновления) останутся доступны в [Google Drive](https://drive.google.com/drive/folders/0B9Ye2auQ_NsFflp6ZHBLSFI2OGVEZ2NQU0pzZkx4SnFmOWlzX0lzcDFjSi1SRk5OdzBYYkU) + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правка + +#### Apply 11_0_fix.patch +> - поправил файл локализации: Spring понимает UTF-8 и не требуются [похдод JSP c escape-кодами](https://stackoverflow.com/questions/8423025/something-wrong-with-encoding-of-properties-or-jsp) +> - В IDEA снимите галочку с [_File->Settings->File Encoding->Transparent native-to-ascii conversion_](https://github.com/JavaOPs/topjava/wiki/IDEA#Поставить-кодировку-utf-8) +> - удалил неиспользуемый импорт + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW10 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW10 +#### Apply 11_01_HW10_fix_encoding.patch +> - Выставлять кодировку `-Dfile.encoding=UTF-8` лучше в _Help menu -> Edit Custom VM Options_ +> - Советы по дополнительным настройкам: +> - [Customize IntelliJ IDEA Memory](http://tomaszdziurko.com/2015/11/1-and-the-only-one-to-customize-intellij-idea-memory-settings) +> - [Increase IDE memory limit in IntelliJ IDEA](http://tomaszdziurko.com/2015/11/1-and-the-only-one-to-customize-intellij-idea-memory-settings) +> - [Slow startup time on Windows](https://youtrack.jetbrains.com/issue/IDEA-211178#focus=streamItem-27-3412218.0-0) + +#### Apply 11_02_HW10_validation.patch +> - В [соответствии со спицификацией](http://stackoverflow.com/a/22358422/548473) для поменял `HTTP 400` (ошибка в структуре сообщения) на `HTTP 422` (ошибка в содержании) +> - Сделал тесты и проверку типа ошибки [через jsonPath](https://www.petrikainulainen.net/programming/spring-framework/integration-testing-of-spring-mvc-applications-write-clean-assertions-with-jsonpath/) + +#### Apply 11_03_HW10_duplicate_email.patch +> - сделал код(ключ) i18n константой (`EXCEPTION_DUPLICATE_EMAIL`) + +#### Apply 11_04_HW10_duplicate_datetime.patch +> - Реализавать обработку дублирования `user.email` и `meal.dateTime` можно по разному +> - через [поиск в сообщении `DataIntegrityViolationException` имени DB constrains](https://stackoverflow.com/a/42422568/548473) +> - через [Controller Based Exception Handling](https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc#controller-based-exception-handling) + +Первый самый простой и расширяемый (хотя зависить от базы), выбрал его. Для работы с HSQLDB сделал `toLowerCase`. + +> - Сделал утильный класс `MessageUtil` для работы с i18n. +> - Добавил тесты на дублирование. Отключил транзакционность в тестах на дублирование через `@Transactional(propagation = Propagation.NEVER)`. +> - [Решение проблемы с транзакционными тестами](https://stackoverflow.com/a/46415060/548473) + +#### Apply 11_05_HW10_binder_validation.patch +> - Добавил корректный способ проверки email с валидатором: проверка делается в контроллерах, а не при сохранении +> - [Spring MVC validator example](https://www.mkyong.com/spring-mvc/spring-mvc-form-handling-example/) + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW10 Optional: change locale +#### Apply 11_06_HW10_change_locale.patch +> - Добавил локализацию календаря `$.datetimepicker.setLocale(localeCode)` +> - Вместо смену локали в `lang.jsp` через javascript сделал `href=${requestScope['javax.servlet.forward.request_uri']}?lang=..` +> - Добавил [Collapsing The Navigation Bar](https://www.w3schools.com/bootstrap4/bootstrap_navbar.asp) + +## Заключительное 11-е занятие + +### Локализация: +#### Apply 11_07_i18n.patch + - Добавил [локализацию Search в datatable](https://datatables.net/reference/option/language) + - Сделал локализацию ошибок валидации: + - Все коды для локализации конкретной ошибки валидации можно посмотреть в дебаге: `FieldError.getCodes()` + - Коды для ее аргументов: `FieldError.getArguments()` (аргумент складывается из имени класса и поля). + - Добавил локализацию `ErrorInfo.type` (код локализации в `ErrorType` и поле `ErrorInfo.typeMessage`) + - В выводе AJAX ошибки вывожу `errorInfo.typeMessage` + - [Увеличил ширину высплывающего noty](https://stackoverflow.com/a/53855189/548473) + +### Защита от XSS (Cross Site Scripting) +> **Попробуйте до и после патча ввести в любое текстовое поле редактирования `` и сохранить.** +#### Apply 11_08_XSS.patch +> - `password` проверять не надо, т.к. он не выводится в html, а [email надо](https://stackoverflow.com/questions/17480809) +> - Сделал общий интерфейс валидации `View.Web` и `@Validated(View.Web.class)` вместо `@Valid` для проверки содержимого только на входе UI/REST. При сохранении проверка на безопасный html контент не делается. + +- XSS для новичков +- XSS глазами злоумышленника +- Prevent people from doing XSS in Spring MVC + +### [Обработка ошибок 404 (NotFound)](https://stackoverflow.com/questions/18322279/spring-mvc-spring-security-and-error-handling) +#### Apply 11_09_404.patch + +### Доступ к AuthorizedUser +#### Apply 11_10_auth_user.patch +- [Автоподстановка в контроллерах](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-authentication-principal) + - не стал делать автоподстановку по всем контроллерам (в абстрактных контроллерах проще работать с `SecurityUtil`, чем получать его через `@AuthenticationPrincipal` и передавать параметром) +- [В JSP: the authentication Tag](https://docs.spring.io/spring-security/site/docs/current/reference/html/taglibs.html#the-authentication-tag) + - авторизованный пользователь доступен в JSP через tag `authentication`, интерсептор становится не нужным + +### Ограничение модификации пользователей +#### Apply 11_11_restrict_modification.patch + - В `AbstractUserController` добавилась защита от удаления-модификации `Admin/User` для профиля `HEROKU` (в `AbstractUserController` заинжектил `Environment` и сделал проверку `HEROKU`) + - Чтобы тесты были рабочими, ввел профиль `HEROKU`, работающий так же, как и `POSTGRES`. + - Добавил `ApplicationException` для работы с ошибками с поддержкой i18n в приложении (от него отнаследовал `NotFoundException` и `ModificationRestrictionException`) + +> Для тестирования с профилем heroku добавьте в VM options: `-Dspring.profiles.active="datajpa,heroku"` + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Деплой приложения в Heroku. +#### Apply 11_12_heroku.patch +> - Добавил зависимости `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 + sslfactory=org.postgresql.ssl.NonValidatingFactory +``` + +- Ресурсы: + - PaaS-платформа Heroku + - Конфигурирование приложения для запуска через Tomcat-based Java Web + - Конфигурирование DataSource profile для Heroku + - Интерграция с IDEA + - 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 + + +### ![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) 5. Разбор типовых собеседований (необработанный вебинар) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание по проекту: +- **Задеплоить свое приложение в Heroku** + - [Маленькие хитрости с Heroku](https://javarush.ru/groups/posts/1987-malenjhkie-khitrosti-s-heroku) +- Сделать / обновить резюме (отдать на ревью в канал #hw11 группы slack) + - [Загрузка сайта на 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/) +- **Заполнить в [своем профиле Java Online Projects](http://javaops.ru/view/login) ссылку на резюме и информацию по поиску работы (если конечно актуально): резюме, флаги рассматриваю работу, готов к релокации и информация для HR.** + - Обновления базы соискателей буду рассылать по HR утром 06.05, постарайтесь успеть +- **Заполнить [форму проверки выпускного проекта](https://docs.google.com/forms/d/1G8cSGBfXIy9bNECo6L-tkxWQYWeVhfzR7te4b-Jwn-Q) (возможно доплатить за ревью отдельно из JavaOPs профиля, как за тестовое собеседование: 1950р)** + - Для участников с проверкой выпускной проект принимаю на ревью до 16.05 (участникам [Masterjava](http://javaops.ru/view/masterjava) до 16.08) +- **Получить первое открытое занятие МНОГОПОТОЧНОСТЬ и пройти эту важную тему в [проекте Masterjava](http://javaops.ru/view/masterjava)** + - Обучение на Masterjava сейчас идет в индивидуальном режиме, старт в любое время. + - Проект, патчи, группа Slack, занятия и видео анологичны проекту Topjava. + - **По 01.05 цена минимальная: 6300 руб.** + +## Возможные доработки приложения: +- Для редактирования паролей сделать отдельный интерфейс с запросом старого пароля и кнопку сброса пароля для администратора. +- Добавление и удаление ролей для пользователей в админке. +- Перевести UI на Angular / Vaadin elements /GWT /GXT /Vaadin / ZK/ [Ваш любимый фреймворк].. +- Перевести проект на Spring Boot 2 +- Перевести шаблоны с JSP на Thymeleaf +- Сделать авторизацию в приложение по OAuth 2.0 (Spring Security OAuth, +VK auth, github oauth, ...) +- Сделать подтверждение регистрации пользователя по email c возможностью восстановления пароля. +- Сделать отображение еды постранично, с поиском и сортировкой на стороне сервера. +- Перевод проекта на 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 + +## Пожалуйста ответь на 4-6 вопросов по проекту, это важно! + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания по резюме: +- варианты размещения: google doc + linkedin + hh + ... +- хорошо, если будет в html формате (например в https://pages.github.com/). [Например так](https://gkislin.github.io/), [на github](https://github.com/gkislin/gkislin.github.io/blob/master/index.html) +- возраст и день рождения можно не писать +- все упоминания Junior убрать! +- линки делай кликабельными (если формат поддерживает) +- если нет опыта в IT, обязательно вставь [участие в стажировке Topjava](https://github.com/JavaOPs/topjava/blob/master/cv.md#Позиционирование-проекта-topjava). Также вставь ссылку на свой сертификат (она есть в личном профиле). Весь не-IT опыт можно кратко. +- всю выгодную для себя информацию (и важную для HR) распологайте вверху. Название секций в резюме и их порядок относительно стандартный и важный +- **Внимание: резюме на hh или других ресурсах ДОЛЖНО БЫТЬ ОТКРЫТО ДЛЯ ПРОСМОТРА и иметь телефон для связи** diff --git a/graduation.md b/graduation.md new file mode 100644 index 000000000000..f167155ab997 --- /dev/null +++ b/graduation.md @@ -0,0 +1,73 @@ +## Выпускной проект +Design and implement a REST API using Hibernate/Spring/SpringMVC (or Spring-Boot) **without frontend**. + +The task is: + +Build a voting system for deciding where to have lunch. + + * 2 types of users: admin and regular users + * Admin can input a restaurant and it's lunch menu of the day (2-5 items usually, just a dish name and price) + * Menu changes each day (admins do the updates) + * Users can vote on which restaurant they want to have lunch at + * Only one vote counted per user + * If user votes again the same day: + - If it is before 11:00 we asume that he changed his mind. + - If it is after 11:00 then it is too late, vote can't be changed + +Each restaurant provides new menu each day. + +As a result, provide a link to github repository. It should contain the code, README.md with API documentation and couple curl commands to test it. + +----------------------------- +P.S.: Make sure everything works with latest version that is on github :) + +P.P.S.: Asume that your API will be used by a frontend developer to build frontend on top of that. + +----------------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Рекомендации + +- Если ты закончил [стажировку Topjava](http://javaops.ru/reg/topjava/grd), **cделай новый проект и добавляй туда из Topjava только то что нужно!** Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - **не нужны!** +- **API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (frontend)** +- **Сначала сделай основной сценарий по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом.** + +*Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас конечно есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если конечно в ТЗ нет оговорок). Все свои вопросы и предложения и хотелки оформляйете отдельно (в `read.me` например). Если делаете что-то сложнее простейшего случая (например справочник еды)- объязательно напишите в read.me. Как и выбор стратегии кэширования.* + +> Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять + +_Антуан де Сент-Экзюпери_ + +- 1: **читаем ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять** +- 2: **тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу** +- 3: **тщательно считайте количество запросов в вашем API для отображения нужной информации** +- 4: **учитывайте, что пользователей может быть ооочень много, а админов- мало** +- 5: в проекте (и тестовом задании на работу) в отличие от нашего учебного topjava оставляйте только необходимый для работы приложения код, ничего лишнего: + - 5.1 НЕ надо делать разные профили базы и работы с ней. + - 5.2 НЕ надо делать абстрактных контроллеров на всякий случай. + - 5.3 НЕ надо делать **классов репозиториев и сервисов**, если там нет ничего, кроме делегирования. + - 5.4 Из потребностей приложения (которую надо самим придумать) реализовывать только очевидные сценарии. Те.- НИЧЕГО ЛИШНЕГО. +- 6: базу лучше взять без установки (H2 или HSQLDB). Ваше приложение должно сразу запуститься **ОПТИМАЛЬНО- без всяких настроек** +- 7: по возможности сделать JUnit тесты +- 8: уделяйте внимание обработке ошибок +- 9: далаем REST API в соответствии с концепцией REST, **с учетом иерархии принадлежности объектов** + - [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) + - 10 Best Practices for Better RESTful API +- 10: не смешивайте TO и Entity вместе. Лучше всего, если они будут независимыми друг от друга. +- 11: если приложению в объекте требуется только его id, используйте reference (как мы при сохранении еды вставляем туда юзера) +- 12: [Use for money in java app](http://stackoverflow.com/a/43051227/548473) +- 13: **Историю еды и голосований сделать НУЖНО! Есть базовые вещи, которые закладываются в архитектуру приложения и неочевидные доработки к ТЗ, которых лучше не делать.** +- 14: Еще раз про [hashCode/equals в Entity](https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma): не делайте сравнение по всем полям! +- 15: Название пакетов, имен классов для `model/to/web` достаточно стандартные (например `model/domain`). НЕ надо придумывать своих собственных правил. +- 16: **Используйте DATA-JPA** (можно без лишней делегации, напрямую из сервиса/контроллера дергать Repository). +- 17: В DATA-JPA 2.x используются `Optional`. Попробуйте работать с ними, это безопасный способ работать с null значениями. +- 18: На topjava мы смотрели разные варианты использования, тут делаем максимально просто. С TO многие вещи упрощаются. +- 19: Проверьте, не торчат ли из кода учебные уши topjava, типа `ProfileRestController.testUTF()`, `AbstractServiceTest.printResult()` или закомментированные `JdbcTemplate`. Назначение этого проекта совсем другое. +- 20: ORM работает с объектами. [В простейших случаях fk_id как поля допустимы](https://stackoverflow.com/questions/6311776/hibernate-foreign-keys-instead-of-entities), но при этом JPA их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverwrflow в коде обязательна! +- 21: Проверьте, станет ли код проще с `@AuthenticationPrincipal` (урок 11, Доступ к AuthorizedUser). +- 22: Не размещайте логику приложения и преобразования в TO в слое доступа к DB +- 23: Если используете кэширование, **тщательно продумайте, что надо кэшировать (самые частые запросы)**, а что нет (большие или редкозапрашиваемые данные)! + +## Попробуйте подергать свое API по всем типичным сценариям ТЗ! +- Удобно использовать? Можно сделать проще? Например чтобы проголосовать за ресторан залогиненному юзеру достаточно `restorauntId`. +- Удовлетворяет ли принципам REST (см. ссылки выше)? +- Сколько раз пришлось его вызвать API для типичного сценария (нарпимер посмотреть рестораны с едой)? +- Сколько запросов к базе было сделано? Можно ли сократить (например с FETCH/Graph или через кэширование)? diff --git a/hr.bat b/hr.bat new file mode 100644 index 000000000000..a7d83b14d89b --- /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 c8a1c78f3b29..435bad736eea 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ru.javawebinar topjava - jar + war 1.0-SNAPSHOT @@ -12,33 +12,428 @@ http://topjava.herokuapp.com/ - 1.8 + 11 UTF-8 UTF-8 + + 5.1.4.RELEASE + 2.1.5.RELEASE + 2.9.8 + 9.0.19 + + + 1.2.3 + 1.7.25 + + + 42.2.5 + + 5.4.1 + 1.3 + 1.5.0 + 2.4.0 + + + 5.4.1.Final + 6.0.15.Final + 1.11.3 + 3.0.1-b11 + + + 3.7.0 + + + 4.3.1 + 3.3.1-2 + 2.5.20 + 3.1.4 + 1.10.19 topjava - install + package org.apache.maven.plugins maven-compiler-plugin - 3.1 + 3.7.0 ${java.version} ${java.version} + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + -Dfile.encoding=UTF-8 + + + + + + + org.codehaus.cargo + cargo-maven2-plugin + 1.7.3 + + + tomcat9x + + UTF-8 + tomcat,datajpa + + + + org.postgresql + postgresql + + + + + + + src/main/resources/tomcat/context.xml + conf/Catalina/localhost/ + context.xml.default + + + + + + ru.javawebinar + topjava + war + + ${project.build.finalName} + + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + runtime + + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + org.springframework + spring-context-support + + + org.springframework.data + spring-data-jpa + ${spring-data-jpa.version} + + + + + org.springframework.security + spring-security-web + ${spring.version} + + + org.springframework.security + spring-security-config + ${spring.version} + + + org.springframework.security + spring-security-taglibs + ${spring.version} + + + + + org.hibernate + hibernate-core + ${hibernate.version} + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + org.jsoup + jsoup + ${jsoup.version} + + + org.hibernate + hibernate-jcache + ${hibernate.version} + + + + + org.glassfish + javax.el + ${javax-el.version} + provided + + + + + javax.cache + cache-api + 1.1.0 + + + org.ehcache + ehcache + runtime + ${ehcache.version} + + + + + org.glassfish.jaxb + jaxb-runtime + 2.4.0-b180830.0438 + + + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + + javax.servlet + jstl + 1.2 + + + + org.springframework + spring-webmvc + + + + + org.webjars + jquery + ${webjars-jquery.version} + + + org.webjars + bootstrap + ${webjars-bootstrap.version} + + + org.webjars + jquery + + + + + org.webjars + datatables + ${webjars-datatables.version} + + + org.webjars + jquery + + + + + org.webjars.bower + datetimepicker + ${webjars-datetimepicker.version} + + + org.webjars.bower + jquery + + + org.webjars.bower + php-date-formatter + + + + + org.webjars.bower + noty + ${webjars-noty.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-json.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate5 + ${jackson-json.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-json.version} + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + + + com.jayway.jsonpath + json-path + ${json-path.version} + test + + + + org.springframework + spring-test + test + + + org.springframework.security + spring-security-test + ${spring.version} + test + + + org.assertj + assertj-core + 3.12.0 + test + + + hsqldb + + + org.hsqldb + hsqldb + 2.4.1 + + + + + postgres + + + org.postgresql + postgresql + ${postgresql.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat.version} + provided + + + + true + + + + heroku + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.1 + + + package + + copy + + + + + + com.github.jsimone + webapp-runner-main + 9.0.19.0 + webapp-runner.jar + + + + + + + + + + + org.postgresql + postgresql + ${postgresql.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat.version} + + + + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + diff --git a/settings.xml b/settings.xml new file mode 100644 index 000000000000..9681d723272c --- /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 new file mode 100644 index 000000000000..a6533838a5e8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java @@ -0,0 +1,33 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; + +public class AuthorizedUser extends org.springframework.security.core.userdetails.User { + private static final long serialVersionUID = 1L; + + private UserTo userTo; + + public AuthorizedUser(User user) { + super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles()); + this.userTo = UserUtil.asTo(user); + } + + public int getId() { + return userTo.getId(); + } + + public void update(UserTo newTo) { + userTo = newTo; + } + + public UserTo getUserTo() { + return userTo; + } + + @Override + public String toString() { + return userTo.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/HasEmail.java b/src/main/java/ru/javawebinar/topjava/HasEmail.java new file mode 100644 index 000000000000..2bc334204b8d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/HasEmail.java @@ -0,0 +1,5 @@ +package ru.javawebinar.topjava; + +public interface HasEmail extends HasId { + String getEmail(); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/HasId.java b/src/main/java/ru/javawebinar/topjava/HasId.java new file mode 100644 index 000000000000..063f0cf2e653 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/HasId.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava; + +public interface HasId { + Integer getId(); + + void setId(Integer id); + + default boolean isNew() { + return getId() == null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java deleted file mode 100644 index b23a2f0961fc..000000000000 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.javawebinar.topjava; - -/** - * User: gkislin - * Date: 05.08.2015 - * - * @link http://caloriesmng.herokuapp.com/ - * @link https://github.com/JavaOPs/topjava - */ -public class Main { - public static void main(String[] args) { - System.out.format("Hello Topjava Enterprise!"); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java new file mode 100644 index 000000000000..91b983429553 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -0,0 +1,30 @@ +package ru.javawebinar.topjava; + +public class Profiles { + public static final String + JDBC = "jdbc", + JPA = "jpa", + DATAJPA = "datajpa"; + + public static final String REPOSITORY_IMPLEMENTATION = DATAJPA; + + public static final String + POSTGRES_DB = "postgres", + HSQL_DB = "hsqldb", + HEROKU = "heroku"; + + // Get DB profile depending of DB driver in classpath + public static String getActiveDbProfile() { + try { + Class.forName("org.postgresql.Driver"); + return POSTGRES_DB; + } catch (ClassNotFoundException ex) { + try { + Class.forName("org.hsqldb.jdbcDriver"); + return Profiles.HSQL_DB; + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Could not find DB driver"); + } + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/View.java b/src/main/java/ru/javawebinar/topjava/View.java new file mode 100644 index 000000000000..f1623c0536b0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/View.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava; + +import javax.validation.groups.Default; + +public class View { + // Validate only form UI/REST + public interface Web extends Default {} + + // Validate only when DB save/update + public interface Persist extends Default {} +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java new file mode 100644 index 000000000000..b5bdf8658c41 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -0,0 +1,62 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.Hibernate; +import ru.javawebinar.topjava.HasId; + +import javax.persistence.*; + +@MappedSuperclass +// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access +@Access(AccessType.FIELD) +//@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE) +public abstract class AbstractBaseEntity implements HasId { + public static final int START_SEQ = 100000; + + @Id + @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) + // @Column(name = "id", unique = true, nullable = false, columnDefinition = "integer default nextval('global_seq')") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") + +// See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034 +// Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting + protected Integer id; + + protected AbstractBaseEntity() { + } + + protected AbstractBaseEntity(Integer id) { + this.id = id; + } + + @Override + public void setId(Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + + @Override + public String toString() { + return String.format("Entity %s (%s)", getClass().getName(), id); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(Hibernate.getClass(o))) { + return false; + } + AbstractBaseEntity that = (AbstractBaseEntity) o; + return id != null && id.equals(that.id); + } + + @Override + public int hashCode() { + return id == null ? 0 : id; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java new file mode 100644 index 000000000000..7186f05fa8f2 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -0,0 +1,41 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.validator.constraints.SafeHtml; +import ru.javawebinar.topjava.View; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + + +@MappedSuperclass +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + @NotBlank + @Size(min = 2, max = 100) + @Column(name = "name", nullable = false) + @SafeHtml(groups = {View.Web.class}) + protected String name; + + protected AbstractNamedEntity() { + } + + protected AbstractNamedEntity(Integer id, String name) { + super(id); + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return String.format("Entity %s (%s, '%s')", getClass().getName(), id, name); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java new file mode 100644 index 000000000000..b0b3ab6bae55 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -0,0 +1,120 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; +import org.springframework.format.annotation.DateTimeFormat; +import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.util.DateTimeUtil; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@SuppressWarnings("JpaQlInspection") +@NamedQueries({ + @NamedQuery(name = Meal.ALL_SORTED, query = "SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC"), + @NamedQuery(name = Meal.DELETE, query = "DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId"), + @NamedQuery(name = Meal.GET_BETWEEN, query = "SELECT m FROM Meal m " + + "WHERE m.user.id=:userId AND m.dateTime BETWEEN :startDate AND :endDate ORDER BY m.dateTime DESC"), +// @NamedQuery(name = Meal.UPDATE, query = "UPDATE Meal m SET m.dateTime = :datetime, m.calories= :calories," + +// "m.description=:desc where m.id=:id and m.user.id=:userId") +}) +@Entity +@Table(name = "meals", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meals_unique_user_datetime_idx")}) +public class Meal extends AbstractBaseEntity { + public static final String ALL_SORTED = "Meal.getAll"; + public static final String DELETE = "Meal.delete"; + public static final String GET_BETWEEN = "Meal.getBetween"; + + @Column(name = "date_time", nullable = false) + @NotNull + @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) + private LocalDateTime dateTime; + + @Column(name = "description", nullable = false) + @NotBlank + @Size(min = 2, max = 120) + @SafeHtml(groups = {View.Web.class}) + private String description; + + @Column(name = "calories", nullable = false) + @Range(min = 10, max = 5000) + @NotNull + private Integer calories; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @NotNull(groups = View.Persist.class) + private User user; + + public Meal() { + } + + public Meal(LocalDateTime dateTime, String description, int calories) { + this(null, dateTime, description, calories); + } + + public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { + super(id); + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public LocalDate getDate() { + return dateTime.toLocalDate(); + } + + public LocalTime getTime() { + return dateTime.toLocalTime(); + } + + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setCalories(Integer calories) { + this.calories = calories; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public String toString() { + return "Meal{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java new file mode 100644 index 000000000000..e7c8739712d1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -0,0 +1,13 @@ +package ru.javawebinar.topjava.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum Role implements GrantedAuthority { + ROLE_USER, + ROLE_ADMIN; + + @Override + public String getAuthority() { + return name(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java new file mode 100644 index 000000000000..70a36cc13f8e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -0,0 +1,161 @@ +package ru.javawebinar.topjava.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; +import org.springframework.util.CollectionUtils; +import ru.javawebinar.topjava.HasEmail; +import ru.javawebinar.topjava.View; + +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.*; + +import static ru.javawebinar.topjava.util.UserUtil.DEFAULT_CALORIES_PER_DAY; + +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@NamedQueries({ + @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), + @NamedQuery(name = User.BY_EMAIL, query = "SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), + @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u ORDER BY u.name, u.email"), +}) +@Entity +@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "users_unique_email_idx")}) +public class User extends AbstractNamedEntity implements HasEmail { + + public static final String DELETE = "User.delete"; + public static final String BY_EMAIL = "User.getByEmail"; + public static final String ALL_SORTED = "User.getAllSorted"; + + @Column(name = "email", nullable = false, unique = true) + @Email + @NotBlank + @Size(max = 100) + @SafeHtml(groups = {View.Web.class}) // https://stackoverflow.com/questions/17480809 + private String email; + + @Column(name = "password", nullable = false) + @NotBlank + @Size(min = 5, max = 100) + // https://stackoverflow.com/a/12505165/548473 + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String password; + + @Column(name = "enabled", nullable = false, columnDefinition = "bool default true") + private boolean enabled = true; + + @Column(name = "registered", nullable = false, columnDefinition = "timestamp default now()") + @NotNull + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Date registered = new Date(); + + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) +// @Fetch(FetchMode.SUBSELECT) + @BatchSize(size = 200) + private Set roles; + + @Column(name = "calories_per_day", columnDefinition = "int default 2000") + @Range(min = 10, max = 10000) + private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) + @OrderBy("dateTime DESC") +// @JsonIgnore + protected List meals; + + public User() { + } + + public User(User u) { + this(u.getId(), u.getName(), u.getEmail(), u.getPassword(), u.getCaloriesPerDay(), u.isEnabled(), u.getRegistered(), u.getRoles()); + } + + public User(Integer id, String name, String email, String password, int caloriesPerDay, Role role, Role... roles) { + this(id, name, email, password, caloriesPerDay, true, new Date(), EnumSet.of(role, roles)); + } + + public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) { + super(id, name); + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + this.enabled = enabled; + this.registered = registered; + setRoles(roles); + } + + @Override + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getRegistered() { + return registered; + } + + public void setRegistered(Date registered) { + this.registered = registered; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getCaloriesPerDay() { + return caloriesPerDay; + } + + public void setCaloriesPerDay(int caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public boolean isEnabled() { + return enabled; + } + + public Set getRoles() { + return roles; + } + + public String getPassword() { + return password; + } + + public void setRoles(Collection roles) { + this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles); + } + + public List getMeals() { + return meals; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", email=" + email + + ", name=" + name + + ", enabled=" + enabled + + ", roles=" + roles + + ", caloriesPerDay=" + caloriesPerDay + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java new file mode 100644 index 000000000000..d740b9f7598e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java @@ -0,0 +1,22 @@ +package ru.javawebinar.topjava.repository; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +public class JpaUtil { + + @PersistenceContext + private EntityManager em; + + public void clear2ndLevelHibernateCache() { + Session s = (Session) em.getDelegate(); + SessionFactory sf = s.getSessionFactory(); +// sf.evict(User.class); +// sf.getCache().evictEntity(User.class, BaseEntity.START_SEQ); +// sf.getCache().evictEntityRegion(User.class); + sf.getCache().evictAllRegions(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java new file mode 100644 index 000000000000..6d717ecbd033 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -0,0 +1,27 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; + +import java.time.LocalDateTime; +import java.util.List; + +public interface MealRepository { + // null if updated meal do not belong to userId + Meal save(Meal meal, int userId); + + // false if meal do not belong to userId + boolean delete(int id, int userId); + + // null if meal do not belong to userId + Meal get(int id, int userId); + + // ORDERED dateTime desc + List getAll(int userId); + + // ORDERED dateTime desc + List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId); + + default Meal getWithUser(int id, int userId) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java new file mode 100644 index 000000000000..6c999c9ed24b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public interface UserRepository { + User save(User user); + + // false if not found + boolean delete(int id); + + // null if not found + User get(int id); + + // null if not found + User getByEmail(String email); + + List getAll(); + + default User getWithMeals(int id) { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java new file mode 100644 index 000000000000..5ae264c292a3 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java @@ -0,0 +1,34 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; + +import java.time.LocalDateTime; +import java.util.List; + +@Transactional(readOnly = true) +public interface CrudMealRepository extends JpaRepository { + + @Modifying + @Transactional + @Query("DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId") + int delete(@Param("id") int id, @Param("userId") int userId); + + @Override + @Transactional + Meal save(Meal item); + + @Query("SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC") + List getAll(@Param("userId") int userId); + + @SuppressWarnings("JpaQlInspection") + @Query("SELECT m from Meal m WHERE m.user.id=:userId AND m.dateTime BETWEEN :startDate AND :endDate ORDER BY m.dateTime DESC") + List getBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("userId") int userId); + + @Query("SELECT m FROM Meal m JOIN FETCH m.user WHERE m.id = ?1 and m.user.id = ?2") + Meal getWithUser(int id, int userId); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java new file mode 100644 index 000000000000..d5474066204f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; + +import java.util.List; +import java.util.Optional; + +@Transactional(readOnly = true) +public interface CrudUserRepository extends JpaRepository { + @Transactional + @Modifying + @Query("DELETE FROM User u WHERE u.id=:id") + int delete(@Param("id") int id); + + @Override + @Transactional + User save(User user); + + @Override + Optional findById(Integer id); + + @Override + List findAll(Sort sort); + + User getByEmail(String email); + + // https://stackoverflow.com/a/46013654/548473 + @EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD) + @Query("SELECT u FROM User u WHERE u.id=?1") + User getWithMeals(int id); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepositoryImpl.java new file mode 100644 index 000000000000..4db0e631cd06 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepositoryImpl.java @@ -0,0 +1,55 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class DataJpaMealRepositoryImpl implements MealRepository { + + @Autowired + private CrudMealRepository crudMealRepository; + + @Autowired + private CrudUserRepository crudUserRepository; + + @Override + @Transactional + public Meal save(Meal meal, int userId) { + if (!meal.isNew() && get(meal.getId(), userId) == null) { + return null; + } + meal.setUser(crudUserRepository.getOne(userId)); + return crudMealRepository.save(meal); + } + + @Override + public boolean delete(int id, int userId) { + return crudMealRepository.delete(id, userId) != 0; + } + + @Override + public Meal get(int id, int userId) { + return crudMealRepository.findById(id).filter(meal -> meal.getUser().getId() == userId).orElse(null); + } + + @Override + public List getAll(int userId) { + return crudMealRepository.getAll(userId); + } + + @Override + public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + return crudMealRepository.getBetween(startDate, endDate, userId); + } + + @Override + public Meal getWithUser(int id, int userId) { + return crudMealRepository.getWithUser(id, userId); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java new file mode 100644 index 000000000000..4e0b1ac7c043 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java @@ -0,0 +1,47 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +@Repository +public class DataJpaUserRepositoryImpl implements UserRepository { + private static final Sort SORT_NAME_EMAIL = new Sort(Sort.Direction.ASC, "name", "email"); + + @Autowired + private CrudUserRepository crudRepository; + + @Override + public User save(User user) { + return crudRepository.save(user); + } + + @Override + public boolean delete(int id) { + return crudRepository.delete(id) != 0; + } + + @Override + public User get(int id) { + return crudRepository.findById(id).orElse(null); + } + + @Override + public User getByEmail(String email) { + return crudRepository.getByEmail(email); + } + + @Override + public List getAll() { + return crudRepository.findAll(SORT_NAME_EMAIL); + } + + @Override + public User getWithMeals(int id) { + return crudRepository.getWithMeals(id); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepositoryImpl.java new file mode 100644 index 000000000000..93c1073315f2 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepositoryImpl.java @@ -0,0 +1,91 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@Transactional(readOnly = true) +public class JdbcMealRepositoryImpl implements MealRepository { + + private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertMeal; + + @Autowired + public JdbcMealRepositoryImpl(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("meals") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Override + @Transactional + public Meal save(Meal meal, int userId) { + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", meal.getId()) + .addValue("description", meal.getDescription()) + .addValue("calories", meal.getCalories()) + .addValue("date_time", meal.getDateTime()) + .addValue("user_id", userId); + + if (meal.isNew()) { + Number newId = insertMeal.executeAndReturnKey(map); + meal.setId(newId.intValue()); + } else { + if (namedParameterJdbcTemplate.update("" + + "UPDATE meals " + + " SET description=:description, calories=:calories, date_time=:date_time " + + " WHERE id=:id AND user_id=:user_id" + , map) == 0) { + return null; + } + } + return meal; + } + + @Override + @Transactional + public boolean delete(int id, int userId) { + return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0; + } + + @Override + public Meal get(int id, int userId) { + List meals = jdbcTemplate.query( + "SELECT * FROM meals WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId); + return DataAccessUtils.singleResult(meals); + } + + @Override + public List getAll(int userId) { + return jdbcTemplate.query( + "SELECT * FROM meals WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId); + } + + @Override + public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + return jdbcTemplate.query( + "SELECT * FROM meals WHERE user_id=? AND date_time BETWEEN ? AND ? ORDER BY date_time DESC", + ROW_MAPPER, userId, startDate, endDate); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java new file mode 100644 index 000000000000..fd9508163468 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java @@ -0,0 +1,119 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.*; + +@Repository +@Transactional(readOnly = true) +public class JdbcUserRepositoryImpl implements UserRepository { + + private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertUser; + + @Autowired + public JdbcUserRepositoryImpl(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertUser = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("users") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Override + @Transactional + public User save(User user) { + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user); + + if (user.isNew()) { + Number newKey = insertUser.executeAndReturnKey(parameterSource); + user.setId(newKey.intValue()); + insertRoles(user); + } else { + if (namedParameterJdbcTemplate.update( + "UPDATE users SET name=:name, email=:email, password=:password, " + + "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", parameterSource) == 0) { + return null; + } + // Simplest implementation. + // More complicated : get user roles from DB and compare them with user.roles (assume that roles are changed rarely). + // If roles are changed, calculate difference in java and delete/insert them. + deleteRoles(user); + insertRoles(user); + } + return user; + } + + @Override + @Transactional + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0; + } + + @Override + public User get(int id) { + List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id); + return setRoles(DataAccessUtils.singleResult(users)); + } + + @Override + public User getByEmail(String email) { +// return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + return setRoles(DataAccessUtils.singleResult(users)); + } + + @Override + public List getAll() { + Map> map = new HashMap<>(); + jdbcTemplate.query("SELECT * FROM user_roles", rs -> { + map.computeIfAbsent(rs.getInt("user_id"), userId -> EnumSet.noneOf(Role.class)) + .add(Role.valueOf(rs.getString("role"))); + }); + List users = jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + users.forEach(u -> u.setRoles(map.get(u.getId()))); + return users; + } + + private void insertRoles(User u) { + Set roles = u.getRoles(); + if (!CollectionUtils.isEmpty(roles)) { + jdbcTemplate.batchUpdate("INSERT INTO user_roles (user_id, role) VALUES (?, ?)", roles, roles.size(), + (ps, role) -> { + ps.setInt(1, u.getId()); + ps.setString(2, role.name()); + }); + } + } + + private void deleteRoles(User u) { + jdbcTemplate.update("DELETE FROM user_roles WHERE user_id=?", u.getId()); + } + + private User setRoles(User u) { + if (u != null) { + List roles = jdbcTemplate.query("SELECT role FROM user_roles WHERE user_id=?", + (rs, rowNum) -> Role.valueOf(rs.getString("role")), u.getId()); + u.setRoles(roles); + } + return u; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepositoryImpl.java new file mode 100644 index 000000000000..2aeeb6356951 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepositoryImpl.java @@ -0,0 +1,65 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.MealRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@Transactional(readOnly = true) +public class JpaMealRepositoryImpl implements MealRepository { + + @PersistenceContext + private EntityManager em; + + @Override + @Transactional + public Meal save(Meal meal, int userId) { + if (!meal.isNew() && get(meal.getId(), userId) == null) { + return null; + } + meal.setUser(em.getReference(User.class, userId)); + if (meal.isNew()) { + em.persist(meal); + return meal; + } else { + return em.merge(meal); + } + } + + @Override + @Transactional + public boolean delete(int id, int userId) { + return em.createNamedQuery(Meal.DELETE) + .setParameter("id", id) + .setParameter("userId", userId) + .executeUpdate() != 0; + } + + @Override + public Meal get(int id, int userId) { + Meal meal = em.find(Meal.class, id); + return meal != null && meal.getUser().getId() == userId ? meal : null; + } + + @Override + public List getAll(int userId) { + return em.createNamedQuery(Meal.ALL_SORTED, Meal.class) + .setParameter("userId", userId) + .getResultList(); + } + + @Override + public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + return em.createNamedQuery(Meal.GET_BETWEEN, Meal.class) + .setParameter("userId", userId) + .setParameter("startDate", startDate) + .setParameter("endDate", endDate).getResultList(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java new file mode 100644 index 000000000000..55752ca362c7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java @@ -0,0 +1,74 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.hibernate.jpa.QueryHints; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; + +@Repository +@Transactional(readOnly = true) +public class JpaUserRepositoryImpl implements UserRepository { + +/* + @Autowired + private SessionFactory sessionFactory; + + private Session openSession() { + return sessionFactory.getCurrentSession(); + } +*/ + + @PersistenceContext + private EntityManager em; + + @Override + @Transactional + public User save(User user) { + if (user.isNew()) { + em.persist(user); + return user; + } else { + return em.merge(user); + } + } + + @Override + public User get(int id) { + return em.find(User.class, id); + } + + @Override + @Transactional + public boolean delete(int id) { + +/* User ref = em.getReference(User.class, id); + em.remove(ref); + + Query query = em.createQuery("DELETE FROM User u WHERE u.id=:id"); + return query.setParameter("id", id).executeUpdate() != 0; +*/ + return em.createNamedQuery(User.DELETE) + .setParameter("id", id) + .executeUpdate() != 0; + } + + @Override + public User getByEmail(String email) { + List users = em.createNamedQuery(User.BY_EMAIL, User.class) + .setParameter(1, email) + .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false) + .getResultList(); + return DataAccessUtils.singleResult(users); + } + + @Override + public List getAll() { + return em.createNamedQuery(User.ALL_SORTED, User.class).getResultList(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java new file mode 100644 index 000000000000..f525c9aabc59 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -0,0 +1,32 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.lang.Nullable; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.DateTimeUtil.adjustEndDateTime; +import static ru.javawebinar.topjava.util.DateTimeUtil.adjustStartDateTime; + +public interface MealService { + Meal get(int id, int userId) throws NotFoundException; + + void delete(int id, int userId) throws NotFoundException; + + default List getBetweenDates(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { + return getBetweenDateTimes(adjustStartDateTime(startDate), adjustEndDateTime(endDate), userId); + } + + List getBetweenDateTimes(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId); + + List getAll(int userId); + + void update(Meal meal, int userId) throws NotFoundException; + + Meal create(Meal meal, int userId); + + Meal getWithUser(int id, int userId); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java new file mode 100644 index 000000000000..04bdb5b518bf --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java @@ -0,0 +1,62 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service +public class MealServiceImpl implements MealService { + + private final MealRepository repository; + + @Autowired + public MealServiceImpl(MealRepository repository) { + this.repository = repository; + } + + @Override + public Meal get(int id, int userId) { + return checkNotFoundWithId(repository.get(id, userId), id); + } + + @Override + public void delete(int id, int userId) { + checkNotFoundWithId(repository.delete(id, userId), id); + } + + @Override + public List getBetweenDateTimes(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + Assert.notNull(startDateTime, "startDateTime must not be null"); + Assert.notNull(endDateTime, "endDateTime must not be null"); + return repository.getBetween(startDateTime, endDateTime, userId); + } + + @Override + public List getAll(int userId) { + return repository.getAll(userId); + } + + @Override + public void update(Meal meal, int userId) { + Assert.notNull(meal, "meal must not be null"); + checkNotFoundWithId(repository.save(meal, userId), meal.getId()); + } + + @Override + public Meal create(Meal meal, int userId) { + Assert.notNull(meal, "meal must not be null"); + return repository.save(meal, userId); + } + + @Override + public Meal getWithUser(int id, int userId) { + return checkNotFoundWithId(repository.getWithUser(id, userId), id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java new file mode 100644 index 000000000000..6a24aa259e75 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.service; + + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.List; + +public interface UserService { + + User create(User user); + + void delete(int id) throws NotFoundException; + + User get(int id) throws NotFoundException; + + User getByEmail(String email) throws NotFoundException; + + void update(User user); + + void update(UserTo user); + + List getAll(); + + void enable(int id, boolean enable); + + User getWithMeals(int id); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java new file mode 100644 index 000000000000..1711a9999b4a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java @@ -0,0 +1,105 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.List; + +import static ru.javawebinar.topjava.util.UserUtil.prepareToSave; +import static ru.javawebinar.topjava.util.UserUtil.updateFromTo; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service("userService") +public class UserServiceImpl implements UserService, UserDetailsService { + + private final UserRepository repository; + private final PasswordEncoder passwordEncoder; + + @Autowired + public UserServiceImpl(UserRepository repository, PasswordEncoder passwordEncoder) { + this.repository = repository; + this.passwordEncoder = passwordEncoder; + } + + + @CacheEvict(value = "users", allEntries = true) + @Override + public User create(User user) { + Assert.notNull(user, "user must not be null"); + return repository.save(prepareToSave(user, passwordEncoder)); + } + + @CacheEvict(value = "users", allEntries = true) + @Override + public void delete(int id) throws NotFoundException { + checkNotFoundWithId(repository.delete(id), id); + } + + @Override + public User get(int id) throws NotFoundException { + return checkNotFoundWithId(repository.get(id), id); + } + + @Override + public User getByEmail(String email) throws NotFoundException { + Assert.notNull(email, "email must not be null"); + return checkNotFound(repository.getByEmail(email), "email=" + email); + } + + @Cacheable("users") + @Override + public List getAll() { + return repository.getAll(); + } + + @CacheEvict(value = "users", allEntries = true) + @Override + public void update(User user) { + Assert.notNull(user, "user must not be null"); + checkNotFoundWithId(repository.save(prepareToSave(user, passwordEncoder)), user.getId()); + } + + @CacheEvict(value = "users", allEntries = true) + @Transactional + @Override + public void update(UserTo userTo) { + User user = updateFromTo(get(userTo.getId()), userTo); + repository.save(prepareToSave(user, passwordEncoder)); + } + + @CacheEvict(value = "users", allEntries = true) + @Override + @Transactional + public void enable(int id, boolean enabled) { + User user = get(id); + user.setEnabled(enabled); + repository.save(user); // !! need only for JDBC implementation + } + + @Override + public AuthorizedUser loadUserByUsername(String email) throws UsernameNotFoundException { + User user = repository.getByEmail(email.toLowerCase()); + if (user == null) { + throw new UsernameNotFoundException("User " + email + " is not found"); + } + return new AuthorizedUser(user); + } + + @Override + public User getWithMeals(int id) { + return checkNotFoundWithId(repository.getWithMeals(id), id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/to/BaseTo.java b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java new file mode 100644 index 000000000000..b7a7de6b7d66 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.to; + +import ru.javawebinar.topjava.HasId; + +public abstract class BaseTo implements HasId { + protected Integer id; + + public BaseTo() { + } + + public BaseTo(Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + + @Override + public void setId(Integer id) { + this.id = id; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java new file mode 100644 index 000000000000..27d118f8e6eb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -0,0 +1,70 @@ +package ru.javawebinar.topjava.to; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class MealTo extends BaseTo { + + private LocalDateTime dateTime; + + private String description; + + private int calories; + + private boolean excess; + + public MealTo() { + } + + public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) { + super(id); + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + this.excess = excess; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public boolean isExcess() { + return excess; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MealTo that = (MealTo) o; + return calories == that.calories && + excess == that.excess && + Objects.equals(id, that.id) && + Objects.equals(dateTime, that.dateTime) && + Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(id, dateTime, description, calories, excess); + } + + @Override + public String toString() { + return "MealTo{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + ", excess=" + excess + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java new file mode 100644 index 000000000000..edfc0659f101 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -0,0 +1,88 @@ +package ru.javawebinar.topjava.to; + +import org.hibernate.validator.constraints.Range; +import ru.javawebinar.topjava.HasEmail; +import org.hibernate.validator.constraints.SafeHtml; +import ru.javawebinar.topjava.util.UserUtil; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; + +public class UserTo extends BaseTo implements HasEmail, Serializable { + private static final long serialVersionUID = 1L; + + @NotBlank + @Size(min = 2, max = 100) + @SafeHtml + private String name; + + @Email + @NotBlank + @Size(max = 100) + @SafeHtml // https://stackoverflow.com/questions/17480809 + private String email; + + @Size(min = 5, max = 32) + private String password; + + @Range(min = 10, max = 10000) + @NotNull + private Integer caloriesPerDay = UserUtil.DEFAULT_CALORIES_PER_DAY; + + public UserTo() { + } + + public UserTo(Integer id, String name, String email, String password, int caloriesPerDay) { + super(id); + this.name = name; + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setCaloriesPerDay(Integer caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public Integer getCaloriesPerDay() { + return caloriesPerDay; + } + + @Override + public String toString() { + return "UserTo{" + + "id=" + id + + ", name='" + name + '\'' + + ", email='" + email + '\'' + + ", caloriesPerDay='" + caloriesPerDay + '\'' + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java new file mode 100644 index 000000000000..25a24b44a1ce --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public class DateTimeUtil { + public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm"; + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); + + // DataBase doesn't support LocalDate.MIN/MAX + private static final LocalDate MIN_DATE = LocalDate.of(1, 1, 1); + private static final LocalDate MAX_DATE = LocalDate.of(3000, 1, 1); + + private DateTimeUtil() { + } + + public static LocalDateTime adjustStartDateTime(LocalDate localDate) { + return adjustDateTime(localDate, MIN_DATE, LocalTime.MIN); + } + + public static LocalDateTime adjustEndDateTime(LocalDate localDate) { + return adjustDateTime(localDate, MAX_DATE, LocalTime.MAX); + } + + private static LocalDateTime adjustDateTime(LocalDate localDate, LocalDate defaultDate, LocalTime adjustTime) { + return LocalDateTime.of(localDate != null ? localDate : defaultDate, adjustTime); + } + + public static String toString(LocalDateTime ldt) { + return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); + } + + public static LocalDate parseLocalDate(@Nullable String str) { + return StringUtils.isEmpty(str) ? null : LocalDate.parse(str); + } + + public static LocalTime parseLocalTime(@Nullable String str) { + return StringUtils.isEmpty(str) ? null : LocalTime.parse(str); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java new file mode 100644 index 000000000000..7c5282399d7d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -0,0 +1,46 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.lang.Nullable; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +public class MealsUtil { + + private MealsUtil() { + } + + public static List getWithExcess(Collection meals, int caloriesPerDay) { + return getFilteredWithExcess(meals, caloriesPerDay, meal -> true); + } + + public static List getFilteredWithExcess(Collection meals, int caloriesPerDay, @Nullable LocalTime startTime, @Nullable LocalTime endTime) { + return getFilteredWithExcess(meals, caloriesPerDay, meal -> Util.isBetween(meal.getTime(), startTime, endTime)); + } + + private static List getFilteredWithExcess(Collection meals, int caloriesPerDay, Predicate filter) { + Map caloriesSumByDate = meals.stream() + .collect( + Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) +// Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum) + ); + + return meals.stream() + .filter(filter) + .map(meal -> createWithExcess(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .collect(toList()); + } + + public static MealTo createWithExcess(Meal meal, boolean excess) { + return new MealTo(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/UserUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java new file mode 100644 index 000000000000..d06ea67c95a6 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java @@ -0,0 +1,35 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.StringUtils; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; + +public class UserUtil { + + public static final int DEFAULT_CALORIES_PER_DAY = 2000; + + public static User createNewFromTo(UserTo newUser) { + return new User(null, newUser.getName(), newUser.getEmail().toLowerCase(), newUser.getPassword(), newUser.getCaloriesPerDay(), Role.ROLE_USER); + } + + public static UserTo asTo(User user) { + return new UserTo(user.getId(), user.getName(), user.getEmail(), user.getPassword(), user.getCaloriesPerDay()); + } + + public static User updateFromTo(User user, UserTo userTo) { + user.setName(userTo.getName()); + user.setEmail(userTo.getEmail().toLowerCase()); + user.setCaloriesPerDay(userTo.getCaloriesPerDay()); + user.setPassword(userTo.getPassword()); + return user; + } + + public static User prepareToSave(User user, PasswordEncoder passwordEncoder) { + String password = user.getPassword(); + user.setPassword(StringUtils.isEmpty(password) ? password : passwordEncoder.encode(password)); + user.setEmail(user.getEmail().toLowerCase()); + return user; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java new file mode 100644 index 000000000000..bc62117d887f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/Util.java @@ -0,0 +1,12 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.lang.Nullable; + +public class Util { + private Util() { + } + + public static > boolean isBetween(T value, @Nullable T start, @Nullable T end) { + return (start == null || value.compareTo(start) >= 0) && (end == null || value.compareTo(end) <= 0); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java new file mode 100644 index 000000000000..d669e8d860fd --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -0,0 +1,74 @@ +package ru.javawebinar.topjava.util; + +import org.slf4j.Logger; +import ru.javawebinar.topjava.HasId; +import ru.javawebinar.topjava.util.exception.ErrorType; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import javax.servlet.http.HttpServletRequest; + +public class ValidationUtil { + + private ValidationUtil() { + } + + public static T checkNotFoundWithId(T object, int id) { + return checkNotFound(object, "id=" + id); + } + + public static void checkNotFoundWithId(boolean found, int id) { + checkNotFound(found, "id=" + id); + } + + public static T checkNotFound(T object, String msg) { + checkNotFound(object != null, msg); + return object; + } + + public static void checkNotFound(boolean found, String arg) { + if (!found) { + throw new NotFoundException(arg); + } + } + + public static void checkNew(HasId bean) { + if (!bean.isNew()) { + throw new IllegalRequestDataException(bean + " must be new (id=null)"); + } + } + + public static void assureIdConsistent(HasId bean, int id) { +// conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473) + if (bean.isNew()) { + bean.setId(id); + } else if (bean.getId() != id) { + throw new IllegalRequestDataException(bean + " must be with id=" + id); + } + } + + // http://stackoverflow.com/a/28565320/548473 + public static Throwable getRootCause(Throwable t) { + Throwable result = t; + Throwable cause; + + while (null != (cause = result.getCause()) && (result != cause)) { + result = cause; + } + return result; + } + + public static String getMessage(Throwable e) { + return e.getLocalizedMessage() != null ? e.getLocalizedMessage() : e.getClass().getName(); + } + + public static Throwable logAndGetRootCause(Logger log, HttpServletRequest req, Exception e, boolean logException, ErrorType errorType) { + Throwable rootCause = ValidationUtil.getRootCause(e); + if (logException) { + log.error(errorType + " at request " + req.getRequestURL(), rootCause); + } else { + log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); + } + return rootCause; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java b/src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java new file mode 100644 index 000000000000..31fb5f20c576 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava.util.exception; + +import org.springframework.http.HttpStatus; + +import java.util.Arrays; + +public class ApplicationException extends RuntimeException { + + private final ErrorType type; + private final String msgCode; + private final HttpStatus httpStatus; + private final String[] args; + + public ApplicationException(String msgCode, HttpStatus httpStatus) { + this(ErrorType.APP_ERROR, msgCode, httpStatus); + } + + public ApplicationException(ErrorType type, String msgCode, HttpStatus httpStatus, String... args) { + super(String.format("type=%s, msgCode=%s, args=%s", type, msgCode, Arrays.toString(args))); + this.type = type; + this.msgCode = msgCode; + this.httpStatus = httpStatus; + this.args = args; + } + + public ErrorType getType() { + return type; + } + + public String getMsgCode() { + return msgCode; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String[] getArgs() { + return args; + } + +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java new file mode 100644 index 000000000000..6a1f7a16a247 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -0,0 +1,15 @@ +package ru.javawebinar.topjava.util.exception; + +public class ErrorInfo { + private final String url; + private final ErrorType type; + private final String typeMessage; + private final String[] details; + + public ErrorInfo(CharSequence url, ErrorType type, String typeMessage, String... details) { + this.url = url.toString(); + this.type = type; + this.typeMessage = typeMessage; + this.details = details; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java new file mode 100644 index 000000000000..a3fa60dbe8c9 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava.util.exception; + +public enum ErrorType { + APP_ERROR("error.appError"), + DATA_NOT_FOUND("error.dataNotFound"), + DATA_ERROR("error.dataError"), + VALIDATION_ERROR("error.validationError"), + WRONG_REQUEST("error.wrongRequest"); + + private final String errorCode; + + ErrorType(String errorCode) { + this.errorCode = errorCode; + } + + public String getErrorCode() { + return errorCode; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java b/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java new file mode 100644 index 000000000000..2b144f91c91c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util.exception; + +public class IllegalRequestDataException extends RuntimeException { + public IllegalRequestDataException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ModificationRestrictionException.java b/src/main/java/ru/javawebinar/topjava/util/exception/ModificationRestrictionException.java new file mode 100644 index 000000000000..2f46bf152dea --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ModificationRestrictionException.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava.util.exception; + +import org.springframework.http.HttpStatus; + +public class ModificationRestrictionException extends ApplicationException { + public static final String EXCEPTION_MODIFICATION_RESTRICTION = "exception.user.modificationRestriction"; + + public ModificationRestrictionException() { + super(EXCEPTION_MODIFICATION_RESTRICTION, HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java new file mode 100644 index 000000000000..94d823ae2a20 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava.util.exception; +import org.springframework.http.HttpStatus; + +public class NotFoundException extends ApplicationException { + public static final String NOT_FOUND_EXCEPTION = "exception.common.notFound"; + + // http://stackoverflow.com/a/22358422/548473 + public NotFoundException(String arg) { + super(ErrorType.DATA_NOT_FOUND, NOT_FOUND_EXCEPTION, HttpStatus.UNPROCESSABLE_ENTITY, arg); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java new file mode 100644 index 000000000000..27e015e81380 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -0,0 +1,100 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ApplicationException; +import ru.javawebinar.topjava.util.exception.ErrorInfo; +import ru.javawebinar.topjava.util.exception.ErrorType; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Optional; + +import static ru.javawebinar.topjava.util.exception.ErrorType.*; + +@RestControllerAdvice(annotations = RestController.class) +@Order(Ordered.HIGHEST_PRECEDENCE + 5) +public class ExceptionInfoHandler { + private static Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); + + public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; + public static final String EXCEPTION_DUPLICATE_DATETIME = "exception.meal.duplicateDateTime"; + + private static final Map CONSTRAINS_I18N_MAP = Map.of( + "users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL, + "meals_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME); + + @Autowired + private MessageUtil messageUtil; + + @ExceptionHandler(ApplicationException.class) + public ResponseEntity applicationError(HttpServletRequest req, ApplicationException appEx) { + ErrorInfo errorInfo = logAndGetErrorInfo(req, appEx, false, appEx.getType(), messageUtil.getMessage(appEx)); + return ResponseEntity.status(appEx.getHttpStatus()).body(errorInfo); + } + + @ResponseStatus(value = HttpStatus.CONFLICT) // 409 + @ExceptionHandler(DataIntegrityViolationException.class) + public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { + String rootMsg = ValidationUtil.getRootCause(e).getMessage(); + if (rootMsg != null) { + String lowerCaseMsg = rootMsg.toLowerCase(); + Optional> entry = CONSTRAINS_I18N_MAP.entrySet().stream() + .filter(it -> lowerCaseMsg.contains(it.getKey())) + .findAny(); + if (entry.isPresent()) { + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, messageUtil.getMessage(entry.get().getValue())); + } + } + return logAndGetErrorInfo(req, e, true, DATA_ERROR); + } + + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class}) + public ErrorInfo bindValidationError(HttpServletRequest req, Exception e) { + BindingResult result = e instanceof BindException ? + ((BindException) e).getBindingResult() : ((MethodArgumentNotValidException) e).getBindingResult(); + + String[] details = result.getFieldErrors().stream() + .map(fe -> messageUtil.getMessage(fe)) + .toArray(String[]::new); + + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, details); + } + + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler({IllegalRequestDataException.class, MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class}) + public ErrorInfo illegalRequestDataError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ErrorInfo handleError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, true, APP_ERROR); + } + + private ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType, String... details) { + Throwable rootCause = ValidationUtil.logAndGetRootCause(log, req, e, logException, errorType); + return new ErrorInfo(req.getRequestURL(), errorType, + messageUtil.getMessage(errorType.getErrorCode()), + details.length != 0 ? details : new String[]{ValidationUtil.getMessage(rootCause)}); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java new file mode 100644 index 000000000000..7f94cf831050 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java @@ -0,0 +1,47 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.NoHandlerFoundException; +import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ApplicationException; +import ru.javawebinar.topjava.util.exception.ErrorType; + +import javax.servlet.http.HttpServletRequest; + +@ControllerAdvice +public class GlobalControllerExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalControllerExceptionHandler.class); + + @Autowired + private MessageUtil messageUtil; + + @ExceptionHandler(NoHandlerFoundException.class) + public ModelAndView wrongRequest(HttpServletRequest req, NoHandlerFoundException e) throws Exception { + return logAndGetExceptionView(req, e, false, ErrorType.WRONG_REQUEST, null); + } + + @ExceptionHandler(ApplicationException.class) + public ModelAndView applicationErrorHandler(HttpServletRequest req, ApplicationException appEx) throws Exception { + return logAndGetExceptionView(req, appEx, true, appEx.getType(), messageUtil.getMessage(appEx)); + } + + @ExceptionHandler(Exception.class) + public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { + return logAndGetExceptionView(req, e, true, ErrorType.APP_ERROR, null); + } + + private ModelAndView logAndGetExceptionView(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType, String msg) { + Throwable rootCause = ValidationUtil.logAndGetRootCause(log, req, e, logException, errorType); + + ModelAndView mav = new ModelAndView("exception/exception"); + mav.addObject("typeMessage", messageUtil.getMessage(errorType.getErrorCode())); + mav.addObject("exception", rootCause); + mav.addObject("message", msg != null ? msg : ValidationUtil.getMessage(rootCause)); + return mav; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java new file mode 100644 index 000000000000..0ed3701537bb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; +import ru.javawebinar.topjava.util.exception.ApplicationException; + +import java.util.Locale; + +@Component +public class MessageUtil { + public static final Locale RU_LOCALE = new Locale("ru"); + + private final MessageSource messageSource; + + @Autowired + public MessageUtil(MessageSource messageSource) { + this.messageSource = messageSource; + } + + public String getMessage(String code, Locale locale, String... args) { + return messageSource.getMessage(code, args, locale); + } + + public String getMessage(String code, String... args) { + return getMessage(code, LocaleContextHolder.getLocale(), args); + } + + public String getMessage(ApplicationException appEx) { + return getMessage(appEx.getMsgCode(), appEx.getArgs()); + } + + public String getMessage(MessageSourceResolvable resolvable) { + return messageSource.getMessage(resolvable, LocaleContextHolder.getLocale()); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java new file mode 100644 index 000000000000..a4f0cc79bad5 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -0,0 +1,77 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.support.SessionStatus; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; +import ru.javawebinar.topjava.web.user.AbstractUserController; + +import javax.validation.Valid; + +@Controller +public class RootController extends AbstractUserController { + + @GetMapping("/") + public String root() { + return "redirect:meals"; + } + + // @Secured("ROLE_ADMIN") + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/users") + public String users() { + return "users"; + } + + @GetMapping(value = "/login") + public String login() { + return "login"; + } + + @GetMapping("/meals") + public String meals() { + return "meals"; + } + + @GetMapping("/profile") + public String profile(ModelMap model, @AuthenticationPrincipal AuthorizedUser authUser) { + model.addAttribute("userTo", authUser.getUserTo()); + return "profile"; + } + + @PostMapping("/profile") + public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status, @AuthenticationPrincipal AuthorizedUser authUser) { + if (result.hasErrors()) { + return "profile"; + } + super.update(userTo, authUser.getId()); + authUser.update(userTo); + status.setComplete(); + return "redirect:meals"; + } + + @GetMapping("/register") + public String register(ModelMap model) { + model.addAttribute("userTo", new UserTo()); + model.addAttribute("register", true); + return "profile"; + } + + @PostMapping("/register") + public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) { + if (result.hasErrors()) { + model.addAttribute("register", true); + return "profile"; + } + super.create(UserUtil.createNewFromTo(userTo)); + status.setComplete(); + return "redirect:login?message=app.registered&username=" + userTo.getEmail(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java new file mode 100644 index 000000000000..861d932264b0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import ru.javawebinar.topjava.AuthorizedUser; + +import static java.util.Objects.requireNonNull; + +public class SecurityUtil { + + private SecurityUtil() { + } + + public static AuthorizedUser safeGet() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null) { + return null; + } + Object principal = auth.getPrincipal(); + return (principal instanceof AuthorizedUser) ? (AuthorizedUser) principal : null; + } + + public static AuthorizedUser get() { + AuthorizedUser user = safeGet(); + requireNonNull(user, "No authorized user found"); + return user; + } + + public static int authUserId() { + return get().getUserTo().getId(); + } + + public static int authUserCaloriesPerDay() { + return get().getUserTo().getCaloriesPerDay(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java new file mode 100644 index 000000000000..bc4409869cce --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava.web.converter; + +import org.springframework.format.Formatter; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +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 parseLocalDate(text); + } + + @Override + public String print(LocalDate lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_DATE); + } + } + + public static class LocalTimeFormatter implements Formatter { + + @Override + public LocalTime parse(String text, Locale locale) { + return parseLocalTime(text); + } + + @Override + public String print(LocalTime lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_TIME); + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java new file mode 100644 index 000000000000..8237df93bffe --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.web.json; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +/** + *

+ * Handling Hibernate lazy-loading + * + * @link https://github.com/FasterXML/jackson + * @link https://github.com/FasterXML/jackson-datatype-hibernate + * @link https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers + */ +public class JacksonObjectMapper extends ObjectMapper { + + private static final ObjectMapper MAPPER = new JacksonObjectMapper(); + + private JacksonObjectMapper() { + registerModule(new Hibernate5Module()); + + registerModule(new JavaTimeModule()); + configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); + setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + public static ObjectMapper getMapper() { + return MAPPER; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java new file mode 100644 index 000000000000..eb7f05b2eeb8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java @@ -0,0 +1,49 @@ +package ru.javawebinar.topjava.web.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectReader; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper; + +public class JsonUtil { + + public static List readValues(String json, Class clazz) { + ObjectReader reader = getMapper().readerFor(clazz); + try { + return reader.readValues(json).readAll(); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid read array from JSON:\n'" + json + "'", e); + } + } + + public static T readValue(String json, Class clazz) { + try { + return getMapper().readValue(json, clazz); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid read from JSON:\n'" + json + "'", e); + } + } + + public static String writeValue(T obj) { + try { + return getMapper().writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e); + } + } + + public static String writeAdditionProps(T obj, String addName, Object addValue) { + return writeAdditionProps(obj, Map.of(addName, addValue)); + } + + public static String writeAdditionProps(T obj, Map addProps) { + Map map = getMapper().convertValue(obj, new TypeReference>() {}); + map.putAll(addProps); + return writeValue(map); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java new file mode 100644 index 000000000000..e27b92a2c1c5 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java @@ -0,0 +1,70 @@ +package ru.javawebinar.topjava.web.meal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.web.SecurityUtil; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +public abstract class AbstractMealController { + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Autowired + private MealService service; + + public Meal get(int id) { + int userId = SecurityUtil.authUserId(); + log.info("get meal {} for user {}", id, userId); + return service.get(id, userId); + } + + public void delete(int id) { + int userId = SecurityUtil.authUserId(); + log.info("delete meal {} for user {}", id, userId); + service.delete(id, userId); + } + + public List getAll() { + int userId = SecurityUtil.authUserId(); + log.info("getAll for user {}", userId); + return MealsUtil.getWithExcess(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay()); + } + + public Meal create(Meal meal) { + int userId = SecurityUtil.authUserId(); + checkNew(meal); + log.info("create {} for user {}", meal, userId); + return service.create(meal, userId); + } + + public void update(Meal meal, int id) { + int userId = SecurityUtil.authUserId(); + assureIdConsistent(meal, id); + log.info("update {} for user {}", meal, userId); + service.update(meal, userId); + } + + /** + *

    Filter separately + *
  1. by date
  2. + *
  3. by time for every date
  4. + *
+ */ + public List getBetween(LocalDate startDate, LocalTime startTime, LocalDate endDate, LocalTime endTime) { + int userId = SecurityUtil.authUserId(); + log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); + + List mealsDateFiltered = service.getBetweenDates(startDate, endDate, userId); + return MealsUtil.getFilteredWithExcess(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java new file mode 100644 index 000000000000..1839daa7e1cf --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -0,0 +1,69 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; + +import java.net.URI; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@RestController +@RequestMapping(value = MealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) +public class MealRestController extends AbstractMealController { + static final String REST_URL = "/rest/profile/meals"; + + @Override + @GetMapping("/{id}") + public Meal get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @Override + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void update(@Validated(View.Web.class) @RequestBody Meal meal, @PathVariable int id) { + super.update(meal, id); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@Validated(View.Web.class) @RequestBody Meal meal) { + Meal created = super.create(meal); + + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + + return ResponseEntity.created(uriOfNewResource).body(created); + } + + @Override + @GetMapping(value = "/filter") + public List getBetween( + @RequestParam(required = false) LocalDate startDate, + @RequestParam(required = false) LocalTime startTime, + @RequestParam(required = false) LocalDate endDate, + @RequestParam(required = false) LocalTime endTime) { + return super.getBetween(startDate, startTime, endDate, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java new file mode 100644 index 000000000000..9fffda7fcb65 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java @@ -0,0 +1,57 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@RestController +@RequestMapping("/ajax/profile/meals") +public class MealUIController extends AbstractMealController { + + @Override + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + return super.getAll(); + } + + @Override + @GetMapping(value = "/{id}") + public Meal get(@PathVariable("id") int id) { + return super.get(id); + } + + @Override + @DeleteMapping(value = "/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @PostMapping + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void createOrUpdate(@Validated(View.Web.class) Meal meal) { + if (meal.isNew()) { + super.create(meal); + } else { + super.update(meal, meal.getId()); + } + } + + @Override + @GetMapping(value = "/filter", produces = MediaType.APPLICATION_JSON_VALUE) + public List getBetween( + @RequestParam(required = false) LocalDate startDate, + @RequestParam(required = false) LocalTime startTime, + @RequestParam(required = false) LocalDate endDate, + @RequestParam(required = false) LocalTime endTime) { + return super.getBetween(startDate, startTime, endDate, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java new file mode 100644 index 000000000000..feb2c980d2d8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -0,0 +1,94 @@ +package ru.javawebinar.topjava.web.user; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.InitBinder; +import ru.javawebinar.topjava.Profiles; +import ru.javawebinar.topjava.model.AbstractBaseEntity; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.exception.ModificationRestrictionException; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +public abstract class AbstractUserController { + protected final Logger log = LoggerFactory.getLogger(getClass()); + + @Autowired + private UserService service; + + @Autowired + private UniqueMailValidator emailValidator; + + private boolean modificationRestriction; + + @Autowired + public void setEnvironment(Environment environment) { + modificationRestriction = environment.acceptsProfiles(Profiles.HEROKU); + } + + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.addValidators(emailValidator); + } + + public List getAll() { + log.info("getAll"); + return service.getAll(); + } + + public User get(int id) { + log.info("get {}", id); + return service.get(id); + } + + public User create(User user) { + log.info("create {}", user); + checkNew(user); + return service.create(user); + } + + public void delete(int id) { + log.info("delete {}", id); + checkModificationAllowed(id); + service.delete(id); + } + + public void update(User user, int id) { + log.info("update {} with id={}", user, id); + assureIdConsistent(user, id); + checkModificationAllowed(id); + service.update(user); + } + + public void update(UserTo userTo, int id) { + log.info("update {} with id={}", userTo, id); + assureIdConsistent(userTo, id); + checkModificationAllowed(id); + service.update(userTo); + } + + public User getByMail(String email) { + log.info("getByEmail {}", email); + return service.getByEmail(email); + } + + public void enable(int id, boolean enabled) { + log.info(enabled ? "enable {}" : "disable {}", id); + checkModificationAllowed(id); + service.enable(id, enabled); + } + + private void checkModificationAllowed(int id) { + if (modificationRestriction && id < AbstractBaseEntity.START_SEQ + 2) { + throw new ModificationRestrictionException(); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java new file mode 100644 index 000000000000..71271adc96ee --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -0,0 +1,61 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.model.User; + +import java.net.URI; +import java.util.List; + +@RestController +@RequestMapping(value = AdminRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) +public class AdminRestController extends AbstractUserController { + + public static final String REST_URL = "/rest/admin/users"; + + @Override + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @GetMapping("/{id}") + public User get(@PathVariable int id) { + return super.get(id); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@Validated(View.Web.class) @RequestBody User user) { + User created = super.create(user); + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + return ResponseEntity.created(uriOfNewResource).body(created); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @Override + @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void update(@Validated(View.Web.class) @RequestBody User user, @PathVariable int id) { + super.update(user, id); + } + + @Override + @GetMapping("/by") + public User getByMail(@RequestParam String email) { + return super.getByMail(email); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java new file mode 100644 index 000000000000..9f449b3cb23c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java @@ -0,0 +1,51 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping("/ajax/admin/users") +public class AdminUIController extends AbstractUserController { + + @Override + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + return super.getAll(); + } + + @Override + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public User get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @PostMapping + public void createOrUpdate(@Valid UserTo userTo) { + if (userTo.isNew()) { + super.create(UserUtil.createNewFromTo(userTo)); + } else { + super.update(userTo, userTo.getId()); + } + } + + @Override + @PostMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void enable(@PathVariable int id, @RequestParam boolean enabled) { + super.enable(id, enabled); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java new file mode 100644 index 000000000000..43af2b7c2dec --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; + +import javax.validation.Valid; +import java.net.URI; + +@RestController +@RequestMapping(ProfileRestController.REST_URL) +public class ProfileRestController extends AbstractUserController { + static final String REST_URL = "/rest/profile"; + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public User get(@AuthenticationPrincipal AuthorizedUser authUser) { + return super.get(authUser.getId()); + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@AuthenticationPrincipal AuthorizedUser authUser) { + super.delete(authUser.getId()); + } + + @PostMapping(value = "/register", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(value = HttpStatus.CREATED) + public ResponseEntity register(@Valid @RequestBody UserTo userTo) { + User created = super.create(UserUtil.createNewFromTo(userTo)); + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + + return ResponseEntity.created(uriOfNewResource).body(created); + } + + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void update(@Valid @RequestBody UserTo userTo, @AuthenticationPrincipal AuthorizedUser authUser) { + super.update(userTo, authUser.getId()); + } + + @GetMapping(value = "/text") + public String testUTF() { + return "Русский текст"; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java b/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java new file mode 100644 index 000000000000..cc56d8c06fa1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java @@ -0,0 +1,32 @@ +package ru.javawebinar.topjava.web.user; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import ru.javawebinar.topjava.HasEmail; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.web.ExceptionInfoHandler; + + +@Component +public class UniqueMailValidator implements org.springframework.validation.Validator { + + @Autowired + private UserRepository repository; + + @Override + public boolean supports(Class clazz) { + return HasEmail.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + HasEmail user = ((HasEmail) target); + User dbUser = repository.getByEmail(user.getEmail().toLowerCase()); + if (dbUser != null && !dbUser.getId().equals(user.getId())) { + errors.rejectValue("email", ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL); + } + } +} diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml new file mode 100644 index 000000000000..c6347bd940d6 --- /dev/null +++ b/src/main/resources/cache/ehcache.xml @@ -0,0 +1,25 @@ + + + + + + + + + + 5 + + 5000 + + + + + + + 1 + + + + diff --git a/src/main/resources/db/heroku.properties b/src/main/resources/db/heroku.properties new file mode 100644 index 000000000000..c8146ba6f139 --- /dev/null +++ b/src/main/resources/db/heroku.properties @@ -0,0 +1,5 @@ +jpa.showSql=false +hibernate.format_sql=false +hibernate.use_sql_comments=false +database.init=false +jdbc.initLocation=initDB.sql \ No newline at end of file diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties new file mode 100644 index 000000000000..3da844ae831f --- /dev/null +++ b/src/main/resources/db/hsqldb.properties @@ -0,0 +1,12 @@ +#database.url=jdbc:hsqldb:file:D:/temp/topjava + +database.driverClassName=org.hsqldb.jdbcDriver +database.url=jdbc:hsqldb:mem:topjava +database.username=sa +database.password= + +database.init=true +jdbc.initLocation=initDB_hsql.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql new file mode 100644 index 000000000000..f39e3d5c3294 --- /dev/null +++ b/src/main/resources/db/initDB.sql @@ -0,0 +1,37 @@ +DROP TABLE IF EXISTS user_roles; +DROP TABLE IF EXISTS meals; +DROP TABLE IF EXISTS users; +DROP SEQUENCE IF EXISTS global_seq; + +CREATE SEQUENCE global_seq START WITH 100000; + +CREATE TABLE users +( + id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), + name VARCHAR NOT NULL, + email VARCHAR NOT NULL, + password VARCHAR NOT NULL, + registered TIMESTAMP DEFAULT now() NOT NULL, + enabled BOOL DEFAULT TRUE NOT NULL, + calories_per_day INTEGER DEFAULT 2000 NOT NULL +); +CREATE UNIQUE INDEX users_unique_email_idx ON users (email); + +CREATE TABLE user_roles +( + user_id INTEGER NOT NULL, + role VARCHAR, + CONSTRAINT user_roles_idx UNIQUE (user_id, role), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE meals ( + id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), + user_id INTEGER NOT NULL, + date_time TIMESTAMP NOT NULL, + description TEXT NOT NULL, + calories INT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX meals_unique_user_datetime_idx + ON meals (user_id, date_time); \ No newline at end of file diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql new file mode 100644 index 000000000000..d40fe769429f --- /dev/null +++ b/src/main/resources/db/initDB_hsql.sql @@ -0,0 +1,41 @@ +DROP TABLE user_roles IF EXISTS; +DROP TABLE meals IF EXISTS; +DROP TABLE users IF EXISTS; +DROP SEQUENCE global_seq IF EXISTS; + +CREATE SEQUENCE GLOBAL_SEQ + AS INTEGER + START WITH 100000; + +CREATE TABLE users +( + id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + registered TIMESTAMP DEFAULT now() NOT NULL, + enabled BOOLEAN DEFAULT TRUE NOT NULL, + calories_per_day INTEGER DEFAULT 2000 NOT NULL +); +CREATE UNIQUE INDEX users_unique_email_idx + ON USERS (email); + +CREATE TABLE user_roles +( + user_id INTEGER NOT NULL, + role VARCHAR(255), + CONSTRAINT user_roles_idx UNIQUE (user_id, role), + FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE +); + +CREATE TABLE meals +( + id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, + date_time TIMESTAMP NOT NULL, + description VARCHAR(255) NOT NULL, + calories INT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX meals_unique_user_datetime_idx + ON meals (user_id, date_time) \ No newline at end of file diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql new file mode 100644 index 000000000000..ed246b066dc8 --- /dev/null +++ b/src/main/resources/db/populateDB.sql @@ -0,0 +1,23 @@ +DELETE FROM user_roles; +DELETE FROM meals; +DELETE FROM users; +ALTER SEQUENCE global_seq RESTART WITH 100000; + +INSERT INTO users (name, email, password, calories_per_day) VALUES + ('User', 'user@yandex.ru', '{noop}password', 2005), + ('Admin', 'admin@gmail.com', '{noop}admin', 1900); + +INSERT INTO user_roles (role, user_id) VALUES + ('ROLE_USER', 100000), + ('ROLE_ADMIN', 100001), + ('ROLE_USER', 100001); + +INSERT INTO meals (date_time, description, calories, user_id) +VALUES ('2015-05-30 10:00:00', 'Завтрак', 500, 100000), + ('2015-05-30 13:00:00', 'Обед', 1000, 100000), + ('2015-05-30 20:00:00', 'Ужин', 500, 100000), + ('2015-05-31 10:00:00', 'Завтрак', 500, 100000), + ('2015-05-31 13:00:00', 'Обед', 1000, 100000), + ('2015-05-31 20:00:00', 'Ужин', 510, 100000), + ('2015-06-01 14:00:00', 'Админ ланч', 510, 100001), + ('2015-06-01 21:00:00', 'Админ ужин', 1500, 100001); diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties new file mode 100644 index 000000000000..0c6c3a05bd87 --- /dev/null +++ b/src/main/resources/db/postgres.properties @@ -0,0 +1,8 @@ +database.url=jdbc:postgresql://localhost:5432/topjava +database.username=user +database.password=P@ssw0rd123 +database.init=true + +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties new file mode 100644 index 000000000000..2e073681ad16 --- /dev/null +++ b/src/main/resources/db/tomcat.properties @@ -0,0 +1,5 @@ +database.init=false +jdbc.initLocation=initDB.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 000000000000..ab4cfe51e08a --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,33 @@ + + + + + + + + ${TOPJAVA_ROOT}/log/topjava.log + + + UTF-8 + %date %-5level %logger{50}.%M:%L - %msg%n + + + + + + UTF-8 + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n + + + + + + + + + + + + + + diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml new file mode 100644 index 000000000000..6af7be0a14a7 --- /dev/null +++ b/src/main/resources/spring/spring-app.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml new file mode 100644 index 000000000000..2ddb38796016 --- /dev/null +++ b/src/main/resources/spring/spring-db.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml new file mode 100644 index 000000000000..08edd2f8751e --- /dev/null +++ b/src/main/resources/spring/spring-mvc.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + text/plain;charset=UTF-8 + text/html;charset=UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml new file mode 100644 index 000000000000..a4fd5b4f21aa --- /dev/null +++ b/src/main/resources/spring/spring-security.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-tools.xml b/src/main/resources/spring/spring-tools.xml new file mode 100644 index 000000000000..96551dc28daf --- /dev/null +++ b/src/main/resources/spring/spring-tools.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/tomcat/context.xml b/src/main/resources/tomcat/context.xml new file mode 100644 index 000000000000..9311d5904aea --- /dev/null +++ b/src/main/resources/tomcat/context.xml @@ -0,0 +1,57 @@ + + + + + + + + WEB-INF/web.xml + ${catalina.base}/conf/web.xml + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/exception/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp new file mode 100644 index 000000000000..d1f5faab895f --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp @@ -0,0 +1,25 @@ +<%@ page isErrorPage="true" contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + +
+
+
+

${typeMessage}

+

${message}

+
+
+ + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp new file mode 100644 index 000000000000..6d0753a6712e --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -0,0 +1,51 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp new file mode 100644 index 000000000000..cf1331fd5523 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp @@ -0,0 +1,8 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%--https://getbootstrap.com/docs/4.0/examples/sticky-footer/--%> +
+
+ +
+
\ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp new file mode 100644 index 000000000000..0cfef281e93b --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -0,0 +1,29 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> + + + + + + + + <spring:message code="app.title"/> + + + + + + + + + + + <%--http://stackoverflow.com/a/24070373/548473--%> + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp new file mode 100644 index 000000000000..398e9e0a89d5 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp @@ -0,0 +1,14 @@ +<%@ page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/login.jsp b/src/main/webapp/WEB-INF/jsp/login.jsp new file mode 100644 index 000000000000..9134ad06410d --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/login.jsp @@ -0,0 +1,81 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> + + + + + + +
+
+ +
${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
+
+ +
+
+ +
+ » + + +
+
+ +
+
+
+    Java Enterprise проект с + регистрацией/авторизацией и интерфейсом на основе ролей (USER, ADMIN). + Администратор может создавать/редактировать/удалять пользователей, а пользователи - управлять своим + профилем и данными (день, еда, калории) через UI (по AJAX) и по REST интерфейсу с базовой авторизацией. + Возможна фильтрация данных по датам и времени, при этом цвет записи таблицы еды зависит от того, превышает ли + сумма + калорий за день норму (редактируемый параметр в профиле пользователя). + Весь REST интерфейс покрывается JUnit тестами, используя Spring MVC Test и Spring Security Test. +
+ + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp new file mode 100644 index 000000000000..fb321e229b92 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -0,0 +1,117 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %> + + + + + + + +
+
+

+ <%--https://getbootstrap.com/docs/4.0/components/card/--%> +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+ + + + + + + + + +
+
+ + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/profile.jsp b/src/main/webapp/WEB-INF/jsp/profile.jsp new file mode 100644 index 000000000000..911b167d38ae --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/profile.jsp @@ -0,0 +1,43 @@ +<%@ page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="topjava" tagdir="/WEB-INF/tags" %> + + + + + + + +
+
+ <%--@elvariable id="userTo" type="ru.javawebinar.topjava.to.UserTo"--%> +
+
+

${userTo.name}

+ + + + + + + +
+ + + + + +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp new file mode 100644 index 000000000000..797498cec365 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -0,0 +1,84 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + + + + + + +
+
+

+ + + + + + + + + + + + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/tags/inputField.tag b/src/main/webapp/WEB-INF/tags/inputField.tag new file mode 100644 index 000000000000..2d8f465488a1 --- /dev/null +++ b/src/main/webapp/WEB-INF/tags/inputField.tag @@ -0,0 +1,19 @@ +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +<%@ attribute name="name" required="true" description="Name of corresponding property in bean object" %> +<%@ attribute name="labelCode" required="true" description="Field label" %> +<%@ attribute name="inputType" required="false" description="Input type" %> + + +
+ + + + + + +
${status.errorMessage}
+
+
\ No newline at end of file diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld new file mode 100644 index 000000000000..d138fecdbfb5 --- /dev/null +++ b/src/main/webapp/WEB-INF/tld/functions.tld @@ -0,0 +1,16 @@ + + + + 1.0 + functions + http://topjava.javawebinar.ru/functions + + + formatDateTime + ru.javawebinar.topjava.util.DateTimeUtil + java.lang.String toString(java.time.LocalDateTime) + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..87c22b88a751 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,71 @@ + + + Topjava + + + spring.profiles.default + postgres,datajpa + + + + contextConfigLocation + + classpath:spring/spring-app.xml + classpath:spring/spring-db.xml + + + + + + org.springframework.web.context.ContextLoaderListener + + + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:spring/spring-mvc.xml + + + throwExceptionIfNoHandlerFound + true + + 1 + + + mvc-dispatcher + / + + + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + + + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + springSecurityFilterChain + /* + + + diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css new file mode 100644 index 000000000000..e7599f2d66b1 --- /dev/null +++ b/src/main/webapp/resources/css/style.css @@ -0,0 +1,55 @@ +tr[data-mealExcess="false"] { + color: green; +} + +tr[data-mealExcess="true"] { + color: red; +} + +.fa { + cursor: pointer; +} + +tr[data-userEnabled="false"] { + opacity: 0.3; +} + +.error, .message { + padding: 10px; + border-radius: 4px; + font-size: 16px; +} + +.error { + color: #a94442; + background-color: #f2dede; + border: 1px solid #ebccd1; +} + +.message { + color: #2f9635; + background-color: #c6fbc2; + border: 1px solid #9feba6; +} + +/*https://stackoverflow.com/a/53855189/548473*/ +#noty_layout__bottomRight { + width: 375px !important; +} + +/*https://getbootstrap.com/docs/4.0/examples/sticky-footer/sticky-footer.css*/ +html { + position: relative; + min-height: 100%; +} +body { + margin-bottom: 60px; /* Margin bottom by footer height */ +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + height: 60px; /* Set the fixed height of the footer here */ + line-height: 60px; /* Vertically center the text there */ + background-color: #f5f5f5; +} \ No newline at end of file diff --git a/src/main/webapp/resources/images/icon-meal.png b/src/main/webapp/resources/images/icon-meal.png new file mode 100644 index 000000000000..b4fc54ad0129 Binary files /dev/null and b/src/main/webapp/resources/images/icon-meal.png differ diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js new file mode 100644 index 000000000000..fa184a7014ba --- /dev/null +++ b/src/main/webapp/resources/js/topjava.common.js @@ -0,0 +1,119 @@ +let context, form; + +function makeEditable(ctx) { + context = ctx; + context.datatableApi = $("#datatable").DataTable( + // https://api.jquery.com/jquery.extend/#jQuery-extend-deep-target-object1-objectN + $.extend(true, ctx.datatableOpts, + { + "ajax": { + "url": context.ajaxUrl, + "dataSrc": "" + }, + "paging": false, + "info": true, + "language": { + "search": i18n["common.search"] + } + } + )); + + form = $('#detailsForm'); + $(document).ajaxError(function (event, jqXHR, options, jsExc) { + failNoty(jqXHR); + }); + + // solve problem with cache in IE: https://stackoverflow.com/a/4303862/548473 + $.ajaxSetup({cache: false}); + + const token = $("meta[name='_csrf']").attr("content"); + const header = $("meta[name='_csrf_header']").attr("content"); + $(document).ajaxSend(function (e, xhr, options) { + xhr.setRequestHeader(header, token); + }); +} + +function add() { + $("#modalTitle").html(i18n["addTitle"]); + form.find(":input").val(""); + $("#editRow").modal(); +} + +function updateRow(id) { + $("#modalTitle").html(i18n["editTitle"]); + $.get(context.ajaxUrl + id, function (data) { + $.each(data, function (key, value) { + form.find("input[name='" + key + "']").val(value); + }); + $('#editRow').modal(); + }); +} + +function deleteRow(id) { + if (confirm(i18n['common.confirm'])) { + $.ajax({ + url: context.ajaxUrl + id, + type: "DELETE" + }).done(function () { + context.updateTable(); + successNoty("common.deleted"); + }); + } +} + +function updateTableByData(data) { + context.datatableApi.clear().rows.add(data).draw(); +} + +function save() { + $.ajax({ + type: "POST", + url: context.ajaxUrl, + data: form.serialize() + }).done(function () { + $("#editRow").modal("hide"); + context.updateTable(); + successNoty("common.saved"); + }); +} + +let failedNote; + +function closeNoty() { + if (failedNote) { + failedNote.close(); + failedNote = undefined; + } +} + +function successNoty(key) { + closeNoty(); + new Noty({ + text: "  " + i18n[key], + type: 'success', + layout: "bottomRight", + timeout: 1000 + }).show(); +} + +function failNoty(jqXHR) { + closeNoty(); + const errorInfo = JSON.parse(jqXHR.responseText); + failedNote = new Noty({ + text: "  " + errorInfo.typeMessage + "
" + errorInfo.details.join("
"), + type: "error", + layout: "bottomRight" + }).show(); +} + +function renderEditBtn(data, type, row) { + if (type === "display") { + return ""; + } +} + +function renderDeleteBtn(data, type, row) { + if (type === "display") { + return ""; + } +} \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.meals.js b/src/main/webapp/resources/js/topjava.meals.js new file mode 100644 index 000000000000..3596cd4d2eb3 --- /dev/null +++ b/src/main/webapp/resources/js/topjava.meals.js @@ -0,0 +1,117 @@ +const mealAjaxUrl = "ajax/profile/meals/"; + +function updateFilteredTable() { + $.ajax({ + type: "GET", + url: mealAjaxUrl + "filter", + data: $("#filter").serialize() + }).done(updateTableByData); +} + +function clearFilter() { + $("#filter")[0].reset(); + $.get(mealAjaxUrl, updateTableByData); +} + +// http://api.jquery.com/jQuery.ajax/#using-converters +$.ajaxSetup({ + converters: { + "text json": function (stringData) { + const json = JSON.parse(stringData); + $(json).each(function () { + this.dateTime = this.dateTime.replace('T', ' ').substr(0, 16); + }); + return json; + } + } +}); + +$(function () { + makeEditable({ + ajaxUrl: mealAjaxUrl, + datatableOpts: { + "columns": [ + { + "data": "dateTime" + }, + { + "data": "description" + }, + { + "data": "calories" + }, + { + "render": renderEditBtn, + "defaultContent": "", + "orderable": false + }, + { + "render": renderDeleteBtn, + "defaultContent": "", + "orderable": false + } + ], + "order": [ + [ + 0, + "desc" + ] + ], + "createdRow": function (row, data, dataIndex) { + $(row).attr("data-mealExcess", data.excess); + }, + }, + updateTable: updateFilteredTable + }); + + $.datetimepicker.setLocale(localeCode); + +// http://xdsoft.net/jqplugins/datetimepicker/ + const startDate = $('#startDate'); + const endDate = $('#endDate'); + startDate.datetimepicker({ + timepicker: false, + format: 'Y-m-d', + formatDate: 'Y-m-d', + onShow: function (ct) { + this.setOptions({ + maxDate: endDate.val() ? endDate.val() : false + }) + } + }); + endDate.datetimepicker({ + timepicker: false, + format: 'Y-m-d', + formatDate: 'Y-m-d', + onShow: function (ct) { + this.setOptions({ + minDate: startDate.val() ? startDate.val() : false + }) + } + }); + + const startTime = $('#startTime'); + const endTime = $('#endTime'); + startTime.datetimepicker({ + datepicker: false, + format: 'H:i', + onShow: function (ct) { + this.setOptions({ + maxTime: endTime.val() ? endTime.val() : false + }) + } + }); + endTime.datetimepicker({ + datepicker: false, + format: 'H:i', + onShow: function (ct) { + this.setOptions({ + minTime: startTime.val() ? startTime.val() : false + }) + } + }); + + $('#dateTime').datetimepicker({ + format: 'Y-m-d H:i' + }); +}); \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js new file mode 100644 index 000000000000..fa3d7f8c556f --- /dev/null +++ b/src/main/webapp/resources/js/topjava.users.js @@ -0,0 +1,85 @@ +const userAjaxUrl = "ajax/admin/users/"; + +function enable(chkbox, id) { + const enabled = chkbox.is(":checked"); +// https://stackoverflow.com/a/22213543/548473 + $.ajax({ + url: userAjaxUrl + id, + type: "POST", + data: "enabled=" + enabled + }).done(function () { + chkbox.closest("tr").attr("data-userEnabled", enabled); + successNoty(enabled ? "common.enabled" : "common.disabled"); + }).fail(function () { + $(chkbox).prop("checked", !enabled); + }); +} + +// $(document).ready(function () { +$(function () { + makeEditable({ + ajaxUrl: userAjaxUrl, + datatableOpts: { + "columns": [ + { + "data": "name" + }, + { + "data": "email", + "render": function (data, type, row) { + if (type === "display") { + return "" + data + ""; + } + return data; + } + }, + { + "data": "roles" + }, + { + "data": "enabled", + "render": function (data, type, row) { + if (type === "display") { + return ""; + } + return data; + } + }, + { + "data": "registered", + "render": function (date, type, row) { + if (type === "display") { + return date.substring(0, 10); + } + return date; + } + }, + { + "orderable": false, + "defaultContent": "", + "render": renderEditBtn + }, + { + "orderable": false, + "defaultContent": "", + "render": renderDeleteBtn + } + ], + "order": [ + [ + 0, + "asc" + ] + ], + "createdRow": function (row, data, dataIndex) { + if (!data.enabled) { + $(row).attr("data-userEnabled", false); + } + } + }, + updateTable: function () { + $.get(userAjaxUrl, updateTableByData); + } + } + ); +}); \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java new file mode 100644 index 000000000000..05d47b19f214 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java @@ -0,0 +1,12 @@ +package ru.javawebinar.topjava; + +import org.springframework.test.context.ActiveProfilesResolver; + +//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver +public class ActiveDbProfileResolver implements ActiveProfilesResolver { + + @Override + public String[] resolve(Class aClass) { + return new String[]{Profiles.getActiveDbProfile()}; + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java b/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java new file mode 100644 index 000000000000..4d1d1e9e3789 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java @@ -0,0 +1,12 @@ +package ru.javawebinar.topjava; + +import org.springframework.test.context.ActiveProfilesResolver; + +//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver +public class AllActiveProfileResolver implements ActiveProfilesResolver { + + @Override + public String[] resolve(Class aClass) { + return new String[]{Profiles.REPOSITORY_IMPLEMENTATION, Profiles.getActiveDbProfile()}; + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java new file mode 100644 index 000000000000..621d34ce3818 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -0,0 +1,57 @@ +package ru.javawebinar.topjava; + +import org.springframework.test.web.servlet.ResultMatcher; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; + +import java.time.Month; +import java.util.List; + +import static java.time.LocalDateTime.of; +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.TestUtil.readListFromJsonMvcResult; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class MealTestData { + public static final int MEAL1_ID = START_SEQ + 2; + public static final int ADMIN_MEAL_ID = START_SEQ + 8; + + public static final Meal MEAL1 = new Meal(MEAL1_ID, of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500); + public static final Meal MEAL2 = new Meal(MEAL1_ID + 1, of(2015, Month.MAY, 30, 13, 0), "Обед", 1000); + public static final Meal MEAL3 = new Meal(MEAL1_ID + 2, of(2015, Month.MAY, 30, 20, 0), "Ужин", 500); + public static final Meal MEAL4 = new Meal(MEAL1_ID + 3, of(2015, Month.MAY, 31, 10, 0), "Завтрак", 500); + public static final Meal MEAL5 = new Meal(MEAL1_ID + 4, of(2015, Month.MAY, 31, 13, 0), "Обед", 1000); + public static final Meal MEAL6 = new Meal(MEAL1_ID + 5, of(2015, Month.MAY, 31, 20, 0), "Ужин", 510); + public static final Meal ADMIN_MEAL1 = new Meal(ADMIN_MEAL_ID, of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510); + public static final Meal ADMIN_MEAL2 = new Meal(ADMIN_MEAL_ID + 1, of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500); + + public static final List MEALS = List.of(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); + + public static Meal getCreated() { + return new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Созданный ужин", 300); + } + + public static Meal getUpdated() { + return new Meal(MEAL1_ID, MEAL1.getDateTime(), "Обновленный завтрак", 200); + } + + public static void assertMatch(Meal actual, Meal expected) { + assertThat(actual).isEqualToIgnoringGivenFields(expected, "user"); + } + + public static void assertMatch(Iterable actual, Meal... expected) { + assertMatch(actual, List.of(expected)); + } + + public static void assertMatch(Iterable actual, Iterable expected) { + assertThat(actual).usingElementComparatorIgnoringFields("user").isEqualTo(expected); + } + + public static ResultMatcher contentJson(MealTo... expected) { + return contentJson(List.of(expected)); + } + + public static ResultMatcher contentJson(Iterable expected) { + return result -> assertThat(readListFromJsonMvcResult(result, MealTo.class)).isEqualTo(expected); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java new file mode 100644 index 000000000000..736194405289 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava; + +import org.springframework.context.support.GenericXmlApplicationContext; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.web.meal.MealRestController; +import ru.javawebinar.topjava.web.user.AdminRestController; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +import static ru.javawebinar.topjava.TestUtil.mockAuthorize; +import static ru.javawebinar.topjava.UserTestData.USER; + +public class SpringMain { + public static void main(String[] args) { + // java 7 automatic resource management + try (GenericXmlApplicationContext appCtx = new GenericXmlApplicationContext()) { + appCtx.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION); + appCtx.load("spring/spring-app.xml", "spring/inmemory.xml"); + appCtx.refresh(); + + mockAuthorize(USER); + + 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", 1500, Role.ROLE_ADMIN)); + System.out.println(); + + MealRestController mealController = appCtx.getBean(MealRestController.class); + List filteredMealsWithExceeded = + mealController.getBetween( + LocalDate.of(2015, Month.MAY, 30), LocalTime.of(7, 0), + LocalDate.of(2015, Month.MAY, 31), LocalTime.of(11, 0)); + filteredMealsWithExceeded.forEach(System.out::println); + } + } +} diff --git a/src/test/java/ru/javawebinar/topjava/TestUtil.java b/src/test/java/ru/javawebinar/topjava/TestUtil.java new file mode 100644 index 000000000000..8103c5b08cae --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.io.UnsupportedEncodingException; +import java.util.List; + +public class TestUtil { + public static String getContent(MvcResult result) throws UnsupportedEncodingException { + return result.getResponse().getContentAsString(); + } + + public static T readFromJson(ResultActions action, Class clazz) throws UnsupportedEncodingException { + return JsonUtil.readValue(getContent(action.andReturn()), clazz); + } + + public static T readFromJsonMvcResult(MvcResult result, Class clazz) throws UnsupportedEncodingException { + return JsonUtil.readValue(getContent(result), clazz); + } + + public static List readListFromJsonMvcResult(MvcResult result, Class clazz) throws UnsupportedEncodingException { + return JsonUtil.readValues(getContent(result), clazz); + } + + public static void mockAuthorize(User user) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(new AuthorizedUser(user), null, user.getRoles())); + } + + public static RequestPostProcessor userHttpBasic(User user) { + return SecurityMockMvcRequestPostProcessors.httpBasic(user.getEmail(), user.getPassword()); + } + + public static RequestPostProcessor userAuth(User user) { + return SecurityMockMvcRequestPostProcessors.authentication(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword())); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/TimingExtension.java b/src/test/java/ru/javawebinar/topjava/TimingExtension.java new file mode 100644 index 000000000000..c5ef4bc00810 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TimingExtension.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava; + +import org.junit.jupiter.api.extension.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StopWatch; + +public class TimingExtension implements + BeforeTestExecutionCallback, AfterTestExecutionCallback, BeforeAllCallback, AfterAllCallback { + + private static final Logger log = LoggerFactory.getLogger("result"); + + private StopWatch stopWatch; + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + stopWatch = new StopWatch("Execution time of " + extensionContext.getRequiredTestClass().getSimpleName()); + } + + @Override + public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { + log.info("Start stopWatch"); + stopWatch.start(extensionContext.getDisplayName()); + } + + @Override + public void afterTestExecution(ExtensionContext extensionContext) throws Exception { + stopWatch.stop(); + log.info("stop stopWatch"); + } + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + log.info('\n' + stopWatch.prettyPrint() + '\n'); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java new file mode 100644 index 000000000000..338ee5640f69 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava; + +import org.springframework.test.web.servlet.ResultMatcher; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.TestUtil.readFromJsonMvcResult; +import static ru.javawebinar.topjava.TestUtil.readListFromJsonMvcResult; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class UserTestData { + public static final int USER_ID = START_SEQ; + public static final int ADMIN_ID = START_SEQ + 1; + + public static final User USER = new User(USER_ID, "User", "user@yandex.ru", "password", 2005, Role.ROLE_USER); + public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", 1900, Role.ROLE_ADMIN, Role.ROLE_USER); + + public static void assertMatch(User actual, User expected) { + assertThat(actual).isEqualToIgnoringGivenFields(expected, "registered", "meals", "password"); + } + + public static void assertMatch(Iterable actual, User... expected) { + assertMatch(actual, List.of(expected)); + } + + public static void assertMatch(Iterable actual, Iterable expected) { + assertThat(actual).usingElementComparatorIgnoringFields("registered", "meals", "password").isEqualTo(expected); + } + + public static ResultMatcher contentJson(User... expected) { + return result -> assertMatch(readListFromJsonMvcResult(result, User.class), List.of(expected)); + } + + public static ResultMatcher contentJson(User expected) { + return result -> assertMatch(readFromJsonMvcResult(result, User.class), expected); + } + + public static String jsonWithPassword(User user, String passw) { + return JsonUtil.writeAdditionProps(user, "password", passw); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepositoryImpl.java new file mode 100644 index 000000000000..ec9f9a516eb3 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepositoryImpl.java @@ -0,0 +1,40 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.AbstractBaseEntity; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +@Repository +public class InMemoryBaseRepositoryImpl { + + private static AtomicInteger counter = new AtomicInteger(0); + + Map entryMap = new ConcurrentHashMap<>(); + + public T save(T entry) { + Objects.requireNonNull(entry, "Entry must not be null"); + if (entry.isNew()) { + entry.setId(counter.incrementAndGet()); + entryMap.put(entry.getId(), entry); + return entry; + } + return entryMap.computeIfPresent(entry.getId(), (id, oldT) -> entry); + } + + public boolean delete(int id) { + return entryMap.remove(id) != null; + } + + public T get(int id) { + return entryMap.get(id); + } + + Collection getCollection() { + return entryMap.values(); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepositoryImpl.java new file mode 100644 index 000000000000..4f3ea3aea460 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepositoryImpl.java @@ -0,0 +1,74 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; +import ru.javawebinar.topjava.util.Util; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Repository +public class InMemoryMealRepositoryImpl implements MealRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepositoryImpl.class); + + // Map userId -> (mealId-> meal) + private Map> usersMealsMap = new ConcurrentHashMap<>(); + + @Override + public Meal save(Meal meal, int userId) { + Objects.requireNonNull(meal, "meal must not be null"); + var meals = usersMealsMap.computeIfAbsent(userId, uid -> new InMemoryBaseRepositoryImpl<>()); + return meals.save(meal); + } + + @PostConstruct + public void postConstruct() { + log.info("+++ PostConstruct"); + } + + @PreDestroy + public void preDestroy() { + log.info("+++ PreDestroy"); + } + + @Override + public boolean delete(int id, int userId) { + var meals = usersMealsMap.get(userId); + return meals != null && meals.delete(id); + } + + @Override + public Meal get(int id, int userId) { + var meals = usersMealsMap.get(userId); + return meals == null ? null : meals.get(id); + } + + @Override + public List getAll(int userId) { + return getAllFiltered(userId, meal -> true); + } + + @Override + public List getBetween(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + Objects.requireNonNull(startDateTime, "startDateTime must not be null"); + Objects.requireNonNull(endDateTime, "endDateTime must not be null"); + return getAllFiltered(userId, meal -> Util.isBetween(meal.getDateTime(), startDateTime, endDateTime)); + } + + private List getAllFiltered(int userId, Predicate filter) { + var meals = usersMealsMap.get(userId); + return meals == null ? Collections.emptyList() : + meals.getCollection().stream() + .filter(filter) + .sorted(Comparator.comparing(Meal::getDateTime).reversed()) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepositoryImpl.java new file mode 100644 index 000000000000..a0bd07e4a7bb --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepositoryImpl.java @@ -0,0 +1,40 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER; + +@Repository +public class InMemoryUserRepositoryImpl extends InMemoryBaseRepositoryImpl implements UserRepository { + + public void init() { + entryMap.clear(); + entryMap.put(UserTestData.USER_ID, USER); + entryMap.put(UserTestData.ADMIN_ID, ADMIN); + } + + @Override + public List getAll() { + return getCollection().stream() + .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) + .collect(Collectors.toList()); + } + + @Override + public User getByEmail(String email) { + Objects.requireNonNull(email, "email must not be null"); + return getCollection().stream() + .filter(u -> email.equals(u.getEmail())) + .findFirst() + .orElse(null); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java new file mode 100644 index 000000000000..79a7a482627f --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java @@ -0,0 +1,34 @@ +package ru.javawebinar.topjava.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.JpaUtil; + +import javax.validation.ConstraintViolationException; +import java.util.Collections; +import java.util.Date; + +abstract public class AbstractJpaUserServiceTest extends AbstractUserServiceTest { + + @Autowired + private JpaUtil jpaUtil; + + @BeforeEach + @Override + void setUp() throws Exception { + super.setUp(); + jpaUtil.clear2ndLevelHibernateCache(); + } + + @Test + void testValidation() throws Exception { + validateRootCause(() -> service.create(new User(null, " ", "invalid@yandex.ru", "password", 2000, Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", " ", "password", 2000, Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "invalid@yandex.ru", "", 2000, Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Collections.emptySet())), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Collections.emptySet())), ConstraintViolationException.class); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java new file mode 100644 index 000000000000..62e2b7e5ce29 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -0,0 +1,95 @@ +package ru.javawebinar.topjava.service; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.util.exception.ErrorType; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import javax.validation.ConstraintViolationException; +import java.time.LocalDate; +import java.time.Month; + +import static java.time.LocalDateTime.of; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +public abstract class AbstractMealServiceTest extends AbstractServiceTest { + + @Autowired + protected MealService service; + + @Test + void delete() throws Exception { + service.delete(MEAL1_ID, USER_ID); + assertMatch(service.getAll(USER_ID), MEAL6, MEAL5, MEAL4, MEAL3, MEAL2); + } + + @Test + void deleteNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.delete(MEAL1_ID, 1)); + } + + @Test + void create() throws Exception { + Meal newMeal = getCreated(); + Meal created = service.create(newMeal, USER_ID); + newMeal.setId(created.getId()); + assertMatch(newMeal, created); + assertMatch(service.getAll(USER_ID), newMeal, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); + } + + @Test + void get() throws Exception { + Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); + assertMatch(actual, ADMIN_MEAL1); + } + + @Test + void getNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.get(MEAL1_ID, ADMIN_ID)); + } + + @Test + void update() throws Exception { + Meal updated = getUpdated(); + service.update(updated, USER_ID); + assertMatch(service.get(MEAL1_ID, USER_ID), updated); + } + + @Test + void updateNotFound() throws Exception { + NotFoundException e = assertThrows(NotFoundException.class, () -> service.update(MEAL1, ADMIN_ID)); + String msg = e.getMessage(); + assertTrue(msg.contains(ErrorType.DATA_NOT_FOUND.name())); + assertTrue(msg.contains(NotFoundException.NOT_FOUND_EXCEPTION)); + assertTrue(msg.contains(String.valueOf(MEAL1_ID))); + } + + @Test + void getAll() throws Exception { + assertMatch(service.getAll(USER_ID), MEALS); + } + + @Test + void getBetween() throws Exception { + assertMatch(service.getBetweenDates( + LocalDate.of(2015, Month.MAY, 30), + LocalDate.of(2015, Month.MAY, 30), USER_ID), MEAL3, MEAL2, MEAL1); + } + + @Test + void testValidation() throws Exception { + Assumptions.assumeTrue(isJpaBased(), "Validation not supported (JPA only)"); + validateRootCause(() -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID), ConstraintViolationException.class); + validateRootCause(() -> service.create(new Meal(null, null, "Description", 300), USER_ID), ConstraintViolationException.class); + validateRootCause(() -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID), ConstraintViolationException.class); + validateRootCause(() -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 5001), USER_ID), ConstraintViolationException.class); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java new file mode 100644 index 000000000000..6c64054ae673 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -0,0 +1,51 @@ +package ru.javawebinar.topjava.service; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.bridge.SLF4JBridgeHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import ru.javawebinar.topjava.ActiveDbProfileResolver; +import ru.javawebinar.topjava.Profiles; +import ru.javawebinar.topjava.TimingExtension; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause; + +@SpringJUnitConfig(locations = { + "classpath:spring/spring-app.xml", + "classpath:spring/spring-db.xml" +}) +//@ExtendWith(SpringExtension.class) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ExtendWith(TimingExtension.class) +abstract class AbstractServiceTest { + + @Autowired + private Environment env; + + static { + // needed only for java.util.logging (postgres driver) + SLF4JBridgeHandler.install(); + } + + boolean isJpaBased() { +// return Arrays.stream(env.getActiveProfiles()).noneMatch(Profiles.JDBC::equals); + return env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.JPA, Profiles.DATAJPA)); + } + + // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 + void validateRootCause(Runnable runnable, Class exceptionClass) { + assertThrows(exceptionClass, () -> { + try { + runnable.run(); + } catch (Exception e) { + throw getRootCause(e); + } + }); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java new file mode 100644 index 000000000000..7e5a4c656754 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -0,0 +1,100 @@ +package ru.javawebinar.topjava.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.dao.DataAccessException; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static ru.javawebinar.topjava.UserTestData.*; + +public abstract class AbstractUserServiceTest extends AbstractServiceTest { + + @Autowired + protected UserService service; + + @Autowired + private CacheManager cacheManager; + + @BeforeEach + void setUp() throws Exception { + cacheManager.getCache("users").clear(); + } + + @Test + void create() throws Exception { + User newUser = new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.ROLE_USER)); + User created = service.create(new User(newUser)); + newUser.setId(created.getId()); + assertMatch(newUser, created); + assertMatch(service.getAll(), ADMIN, newUser, USER); + } + + @Test + void duplicateMailCreate() throws Exception { + assertThrows(DataAccessException.class, () -> + service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", 2000, Role.ROLE_USER))); + } + + @Test + void delete() throws Exception { + service.delete(USER_ID); + assertMatch(service.getAll(), ADMIN); + } + + @Test + void deletedNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.delete(1)); + } + + @Test + void get() throws Exception { + User user = service.get(ADMIN_ID); + assertMatch(user, ADMIN); + } + + @Test + void getNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.get(1)); + } + + @Test + void getByEmail() throws Exception { + User user = service.getByEmail("admin@gmail.com"); + assertMatch(user, ADMIN); + } + + @Test + void update() throws Exception { + User updated = new User(USER); + updated.setName("UpdatedName"); + updated.setCaloriesPerDay(330); + updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN)); + service.update(new User(updated)); + assertMatch(service.get(USER_ID), updated); + } + + @Test + void getAll() throws Exception { + List all = service.getAll(); + assertMatch(all, ADMIN, USER); + } + + @Test + void enable() { + service.enable(USER_ID, false); + assertFalse(service.get(USER_ID).isEnabled()); + service.enable(USER_ID, true); + assertTrue(service.get(USER_ID).isEnabled()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java new file mode 100644 index 000000000000..b14f40f2f126 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; + +@ActiveProfiles(DATAJPA) +class DataJpaMealServiceTest extends AbstractMealServiceTest { + @Test + void testGetWithUser() throws Exception { + Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID); + assertMatch(adminMeal, ADMIN_MEAL1); + UserTestData.assertMatch(adminMeal.getUser(), UserTestData.ADMIN); + } + + @Test + void testGetWithUserNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.getWithUser(MEAL1_ID, ADMIN_ID)); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java new file mode 100644 index 000000000000..53d3759eeeec --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -0,0 +1,28 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.*; + +@ActiveProfiles(DATAJPA) +class DataJpaUserServiceTest extends AbstractJpaUserServiceTest { + @Test + void testGetWithMeals() throws Exception { + User admin = service.getWithMeals(ADMIN_ID); + assertMatch(admin, ADMIN); + MealTestData.assertMatch(admin.getMeals(), MealTestData.ADMIN_MEAL2, MealTestData.ADMIN_MEAL1); + } + + @Test + void testGetWithMealsNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.getWithMeals(1)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java new file mode 100644 index 000000000000..aef588264f71 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles(JDBC) +class JdbcMealServiceTest extends AbstractMealServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java new file mode 100644 index 000000000000..62ca7668cf67 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles(JDBC) +class JdbcUserServiceTest extends AbstractUserServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java new file mode 100644 index 000000000000..aaf5dcda960e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles(JPA) +class JpaMealServiceTest extends AbstractMealServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java new file mode 100644 index 000000000000..d903cf67a6dd --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles(JPA) +class JpaUserServiceTest extends AbstractJpaUserServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java new file mode 100644 index 000000000000..5f12928a26df --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -0,0 +1,87 @@ +package ru.javawebinar.topjava.web; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import ru.javawebinar.topjava.AllActiveProfileResolver; +import ru.javawebinar.topjava.repository.JpaUtil; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.util.exception.ErrorType; + +import javax.annotation.PostConstruct; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +@SpringJUnitWebConfig(locations = { + "classpath:spring/spring-app.xml", + "classpath:spring/spring-mvc.xml", + "classpath:spring/spring-db.xml" +}) +//@WebAppConfiguration +//@ExtendWith(SpringExtension.class) +@Transactional +@ActiveProfiles(resolver = AllActiveProfileResolver.class) +abstract public class AbstractControllerTest { + + private static final CharacterEncodingFilter CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter(); + + static { + CHARACTER_ENCODING_FILTER.setEncoding("UTF-8"); + CHARACTER_ENCODING_FILTER.setForceEncoding(true); + } + + protected MockMvc mockMvc; + + @Autowired + private CacheManager cacheManager; + + @Autowired(required = false) + private JpaUtil jpaUtil; + + @Autowired + protected UserService userService; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + protected MessageUtil messageUtil; + + @PostConstruct + private void postConstruct() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .addFilter(CHARACTER_ENCODING_FILTER) + .apply(springSecurity()) + .build(); + } + + @BeforeEach + void setUp() { + cacheManager.getCache("users").clear(); + if (jpaUtil != null) { + jpaUtil.clear2ndLevelHibernateCache(); + } + } + + private String getMessage(String code) { + return messageUtil.getMessage(code, MessageUtil.RU_LOCALE); + } + + public ResultMatcher errorType(ErrorType type) { + return jsonPath("$.type").value(type.name()); + } + + public ResultMatcher detailMessage(String code) { + return jsonPath("$.details").value(getMessage(code)); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java new file mode 100644 index 000000000000..adde02f705c0 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.web; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ResourceControllerTest extends AbstractControllerTest { + + @Test + void testResources() throws Exception { + mockMvc.perform(get("/resources/css/style.css")) + .andDo(print()) + .andExpect(content().contentType(MediaType.valueOf("text/css"))) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java new file mode 100644 index 000000000000..891aadc11aed --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -0,0 +1,41 @@ +package ru.javawebinar.topjava.web; + +import org.junit.jupiter.api.Test; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static ru.javawebinar.topjava.TestUtil.userAuth; +import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER; + +class RootControllerTest extends AbstractControllerTest { + + @Test + void testUsers() throws Exception { + mockMvc.perform(get("/users") + .with(userAuth(ADMIN))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("users")) + .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")); + } + + @Test + void testUnAuth() throws Exception { + mockMvc.perform(get("/users")) + .andDo(print()) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + void testMeals() throws Exception { + mockMvc.perform(get("/meals") + .with(userAuth(USER))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("meals")) + .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp")); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java new file mode 100644 index 000000000000..a7c2a6a515cb --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava.web.json; + +import org.junit.jupiter.api.Test; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static ru.javawebinar.topjava.MealTestData.*; + +class JsonUtilTest { + + @Test + void testReadWriteValue() throws Exception { + String json = JsonUtil.writeValue(ADMIN_MEAL1); + System.out.println(json); + Meal meal = JsonUtil.readValue(json, Meal.class); + assertMatch(meal, ADMIN_MEAL1); + } + + @Test + void testReadWriteValues() throws Exception { + String json = JsonUtil.writeValue(MEALS); + System.out.println(json); + List meals = JsonUtil.readValues(json, Meal.class); + assertMatch(meals, MEALS); + } + + @Test + void testWriteOnlyAccess() throws Exception { + String json = JsonUtil.writeValue(UserTestData.USER); + System.out.println(json); + assertThat(json, not(containsString("password"))); + String jsonWithPass = UserTestData.jsonWithPassword(UserTestData.USER, "newPass"); + System.out.println(jsonWithPass); + User user = JsonUtil.readValue(jsonWithPass, User.class); + assertEquals(user.getPassword(), "newPass"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java new file mode 100644 index 000000000000..8a966558b791 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -0,0 +1,202 @@ +package ru.javawebinar.topjava.web.meal; + + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.time.LocalDateTime; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.MealTestData.assertMatch; +import static ru.javawebinar.topjava.MealTestData.contentJson; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.TestUtil.*; +import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; +import static ru.javawebinar.topjava.util.MealsUtil.createWithExcess; +import static ru.javawebinar.topjava.util.MealsUtil.getWithExcess; +import static ru.javawebinar.topjava.util.exception.ErrorType.VALIDATION_ERROR; +import static ru.javawebinar.topjava.web.ExceptionInfoHandler.EXCEPTION_DUPLICATE_DATETIME; + +class MealRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = MealRestController.REST_URL + '/'; + + @Autowired + private MealService service; + + @Test + void testGet() throws Exception { + mockMvc.perform(get(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(result -> assertMatch(readFromJsonMvcResult(result, Meal.class), ADMIN_MEAL1)); + } + + @Test + void testGetUnauth() throws Exception { + mockMvc.perform(get(REST_URL + MEAL1_ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + void testGetNotFound() throws Exception { + mockMvc.perform(get(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(USER))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void testDelete() throws Exception { + mockMvc.perform(delete(REST_URL + MEAL1_ID) + .with(userHttpBasic(USER))) + .andExpect(status().isNoContent()); + assertMatch(service.getAll(START_SEQ), MEAL6, MEAL5, MEAL4, MEAL3, MEAL2); + } + + @Test + void testDeleteNotFound() throws Exception { + mockMvc.perform(delete(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void testUpdate() throws Exception { + Meal updated = getUpdated(); + + mockMvc.perform(put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated)) + .with(userHttpBasic(USER))) + .andExpect(status().isNoContent()); + + assertMatch(service.get(MEAL1_ID, START_SEQ), updated); + } + + @Test + void testCreate() throws Exception { + Meal created = getCreated(); + ResultActions action = mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(created)) + .with(userHttpBasic(ADMIN))); + + Meal returned = readFromJson(action, Meal.class); + created.setId(returned.getId()); + + assertMatch(returned, created); + assertMatch(service.getAll(ADMIN_ID), ADMIN_MEAL2, created, ADMIN_MEAL1); + } + + @Test + void testGetAll() throws Exception { + mockMvc.perform(get(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(contentJson(getWithExcess(MEALS, USER.getCaloriesPerDay()))); + } + + @Test + void testFilter() throws Exception { + mockMvc.perform(get(REST_URL + "filter") + .param("startDate", "2015-05-30").param("startTime", "07:00") + .param("endDate", "2015-05-31").param("endTime", "11:00") + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(contentJson(createWithExcess(MEAL4, true), createWithExcess(MEAL1, false))); + } + + @Test + void testFilterAll() throws Exception { + mockMvc.perform(get(REST_URL + "filter?startDate=&endTime=") + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andExpect(contentJson(getWithExcess(MEALS, USER.getCaloriesPerDay()))); + } + + @Test + void testCreateInvalid() throws Exception { + Meal invalid = new Meal(null, null, "Dummy", 200); + mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(VALIDATION_ERROR)) + .andDo(print()); + } + + @Test + void testUpdateInvalid() throws Exception { + Meal invalid = new Meal(MEAL1_ID, null, null, 6000); + mockMvc.perform(put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(VALIDATION_ERROR)) + .andDo(print()); + } + + @Test + @Transactional(propagation = Propagation.NEVER) + void testUpdateDuplicate() throws Exception { + Meal invalid = new Meal(MEAL1_ID, MEAL2.getDateTime(), "Dummy", 200); + + mockMvc.perform(put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isConflict()) + .andExpect(errorType(VALIDATION_ERROR)) + .andExpect(detailMessage(EXCEPTION_DUPLICATE_DATETIME)); + } + + @Test + @Transactional(propagation = Propagation.NEVER) + void testCreateDuplicate() throws Exception { + Meal invalid = new Meal(null, ADMIN_MEAL1.getDateTime(), "Dummy", 200); + mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isConflict()) + .andExpect(errorType(VALIDATION_ERROR)) + .andExpect(detailMessage(EXCEPTION_DUPLICATE_DATETIME)); + } + + @Test + void testUpdateHtmlUnsafe() throws Exception { + Meal invalid = new Meal(MEAL1_ID, LocalDateTime.now(), "", 200); + mockMvc.perform(put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(VALIDATION_ERROR)) + .andDo(print()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java new file mode 100644 index 000000000000..b190aad7477b --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -0,0 +1,180 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.util.Collections; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.TestUtil.readFromJson; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.util.exception.ErrorType.VALIDATION_ERROR; +import static ru.javawebinar.topjava.web.ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL; + +class AdminRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = AdminRestController.REST_URL + '/'; + + @Test + void testGet() throws Exception { + mockMvc.perform(get(REST_URL + ADMIN_ID) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andDo(print()) + // https://jira.spring.io/browse/SPR-14472 + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(contentJson(ADMIN)); + } + + @Test + void testGetNotFound() throws Exception { + mockMvc.perform(get(REST_URL + 1) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isUnprocessableEntity()) + .andDo(print()); + } + + @Test + void testGetByEmail() throws Exception { + mockMvc.perform(get(REST_URL + "by?email=" + ADMIN.getEmail()) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(contentJson(ADMIN)); + } + + @Test + void testDelete() throws Exception { + mockMvc.perform(delete(REST_URL + USER_ID) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isNoContent()); + assertMatch(userService.getAll(), ADMIN); + } + + @Test + void testDeleteNotFound() throws Exception { + mockMvc.perform(delete(REST_URL + 1) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isUnprocessableEntity()) + .andDo(print()); + } + + @Test + void testGetUnAuth() throws Exception { + mockMvc.perform(get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + void testGetForbidden() throws Exception { + mockMvc.perform(get(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isForbidden()); + } + + @Test + void testUpdate() throws Exception { + User updated = new User(USER); + updated.setName("UpdatedName"); + updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN)); + mockMvc.perform(put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(jsonWithPassword(updated, USER.getPassword()))) + .andExpect(status().isNoContent()); + + assertMatch(userService.get(USER_ID), updated); + } + + @Test + void testCreate() throws Exception { + User expected = new User(null, "New", "new@gmail.com", "newPass", 2300, Role.ROLE_USER, Role.ROLE_ADMIN); + ResultActions action = mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(jsonWithPassword(expected, "newPass"))) + .andExpect(status().isCreated()); + + User returned = readFromJson(action, User.class); + expected.setId(returned.getId()); + + assertMatch(returned, expected); + assertMatch(userService.getAll(), ADMIN, expected, USER); + } + + @Test + void testGetAll() throws Exception { + mockMvc.perform(get(REST_URL) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(contentJson(ADMIN, USER)); + } + + @Test + void testCreateInvalid() throws Exception { + User expected = new User(null, null, "", "newPass", 7300, Role.ROLE_USER, Role.ROLE_ADMIN); + mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(JsonUtil.writeValue(expected))) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(VALIDATION_ERROR)) + .andDo(print()); + } + + @Test + void testUpdateInvalid() throws Exception { + User updated = new User(USER); + updated.setName(""); + mockMvc.perform(put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(JsonUtil.writeValue(updated))) + .andExpect(status().isUnprocessableEntity()) + .andDo(print()) + .andExpect(errorType(VALIDATION_ERROR)) + .andDo(print()); + } + + @Test + @Transactional(propagation = Propagation.NEVER) + void testUpdateDuplicate() throws Exception { + User updated = new User(USER); + updated.setEmail("admin@gmail.com"); + mockMvc.perform(put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(jsonWithPassword(updated, "password"))) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(VALIDATION_ERROR)) + .andExpect(detailMessage(EXCEPTION_DUPLICATE_EMAIL)) + .andDo(print()); + } + + @Test + @Transactional(propagation = Propagation.NEVER) + void testCreateDuplicate() throws Exception { + User expected = new User(null, "New", "user@yandex.ru", "newPass", 2300, Role.ROLE_USER, Role.ROLE_ADMIN); + mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(jsonWithPassword(expected, "newPass"))) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(VALIDATION_ERROR)) + .andExpect(detailMessage(EXCEPTION_DUPLICATE_EMAIL)); + + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/HerokuRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/HerokuRestControllerTest.java new file mode 100644 index 000000000000..cc7f82240fa8 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/HerokuRestControllerTest.java @@ -0,0 +1,67 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePropertySource; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.util.exception.ErrorType; +import ru.javawebinar.topjava.web.AbstractControllerTest; + +import java.io.IOException; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.Profiles.HEROKU; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.util.exception.ModificationRestrictionException.EXCEPTION_MODIFICATION_RESTRICTION; + +@ActiveProfiles({HEROKU}) +class HerokuRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = AdminRestController.REST_URL + '/'; + + // Set DATABASE_URL environment for heroku profile + static { + Resource resource = new ClassPathResource("db/postgres.properties"); + try { + PropertySource propertySource = new ResourcePropertySource(resource); + String herokuDbUrl = String.format("postgres://%s:%s@%s", + propertySource.getProperty("database.username"), + propertySource.getProperty("database.password"), + ((String) propertySource.getProperty("database.url")).substring(18)); + System.out.println(herokuDbUrl); + + System.setProperty("DATABASE_URL", herokuDbUrl); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Test + void testDelete() throws Exception { + mockMvc.perform(delete(REST_URL + USER_ID) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(errorType(ErrorType.APP_ERROR)) + .andExpect(detailMessage(EXCEPTION_MODIFICATION_RESTRICTION)) + .andExpect(status().isUnavailableForLegalReasons()); + } + + @Test + void testUpdate() throws Exception { + mockMvc.perform(put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(UserTestData.jsonWithPassword(USER, "password"))) + .andExpect(errorType(ErrorType.APP_ERROR)) + .andExpect(detailMessage(EXCEPTION_MODIFICATION_RESTRICTION)) + .andExpect(status().isUnavailableForLegalReasons()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java new file mode 100644 index 000000000000..2b98dfc0d2b5 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -0,0 +1,46 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepositoryImpl; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import ru.javawebinar.topjava.web.user.AdminRestController; + +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.javawebinar.topjava.UserTestData.ADMIN; + +@SpringJUnitConfig(locations = {"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) +class InMemoryAdminRestControllerSpringTest { + + @Autowired + private AdminRestController controller; + + @Autowired + private InMemoryUserRepositoryImpl repository; + + @BeforeEach + void setUp() throws Exception { + repository.init(); + } + + @Test + void testDelete() throws Exception { + controller.delete(UserTestData.USER_ID); + Collection users = controller.getAll(); + assertEquals(users.size(), 1); + assertEquals(users.iterator().next(), ADMIN); + } + + @Test + void testDeleteNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + controller.delete(10)); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java new file mode 100644 index 000000000000..d502c023a68d --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -0,0 +1,59 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepositoryImpl; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.javawebinar.topjava.UserTestData.ADMIN; + +class InMemoryAdminRestControllerTest { + private static ConfigurableApplicationContext appCtx; + private static AdminRestController controller; + + @BeforeAll + static void beforeClass() { + appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml"); + System.out.println("\n" + Arrays.toString(appCtx.getBeanDefinitionNames()) + "\n"); + controller = appCtx.getBean(AdminRestController.class); + } + + @AfterAll + static void afterClass() { +// May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed +// http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite +// appCtx.close(); + } + + @BeforeEach + void setUp() throws Exception { + // re-initialize + InMemoryUserRepositoryImpl repository = appCtx.getBean(InMemoryUserRepositoryImpl.class); + repository.init(); + } + + @Test + void delete() throws Exception { + controller.delete(UserTestData.USER_ID); + Collection users = controller.getAll(); + assertEquals(users.size(), 1); + assertEquals(users.iterator().next(), ADMIN); + } + + @Test + void deleteNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + controller.delete(10)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java new file mode 100644 index 000000000000..a0207f560744 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -0,0 +1,106 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.TestUtil.readFromJson; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.util.exception.ErrorType.VALIDATION_ERROR; +import static ru.javawebinar.topjava.web.ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL; +import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; + +class ProfileRestControllerTest extends AbstractControllerTest { + + @Test + void testGet() throws Exception { + mockMvc.perform(get(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(contentJson(USER)); + } + + @Test + void testGetUnAuth() throws Exception { + mockMvc.perform(get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + void testDelete() throws Exception { + mockMvc.perform(delete(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isNoContent()); + assertMatch(userService.getAll(), ADMIN); + } + + @Test + void testRegister() throws Exception { + UserTo createdTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500); + + ResultActions action = mockMvc.perform(post(REST_URL + "/register").contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(createdTo))) + .andDo(print()) + .andExpect(status().isCreated()); + User returned = readFromJson(action, User.class); + + User created = UserUtil.createNewFromTo(createdTo); + created.setId(returned.getId()); + + assertMatch(returned, created); + assertMatch(userService.getByEmail("newemail@ya.ru"), created); + } + + @Test + void testUpdate() throws Exception { + UserTo updatedTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500); + + mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER)) + .content(JsonUtil.writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + assertMatch(userService.getByEmail("newemail@ya.ru"), UserUtil.updateFromTo(new User(USER), updatedTo)); + } + + @Test + void testUpdateInvalid() throws Exception { + UserTo updatedTo = new UserTo(null, null, "password", null, 1500); + + mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER)) + .content(JsonUtil.writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(VALIDATION_ERROR)) + .andDo(print()); + } + + @Test + @Transactional(propagation = Propagation.NEVER) + void testDuplicate() throws Exception { + UserTo updatedTo = new UserTo(null, "newName", "admin@gmail.com", "newPassword", 1500); + + mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER)) + .content(JsonUtil.writeValue(updatedTo))) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(VALIDATION_ERROR)) + .andExpect(detailMessage(EXCEPTION_DUPLICATE_EMAIL)) + .andDo(print()); + } +} \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..019d8a1edcb6 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,33 @@ + + + + true + + + + + UTF-8 + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n + + + + + + UTF-8 + %magenta(%msg%n) + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml new file mode 100644 index 000000000000..5798d6f5ac1c --- /dev/null +++ b/src/test/resources/spring/inmemory.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/system.properties b/system.properties new file mode 100644 index 000000000000..180a27348d82 --- /dev/null +++ b/system.properties @@ -0,0 +1 @@ +java.runtime.version=11 \ No newline at end of file