From c8ee764c6babbc0ce65e3e42aee31c085534ce72 Mon Sep 17 00:00:00 2001 From: JavaOPs Date: Wed, 23 Jan 2019 17:44:44 +0300 Subject: [PATCH 01/97] Update info --- README.md | 198 ++++++++++++++++++++++++++++++++++++++++++++++++ ReleaseNotes.md | 179 +++++++++++++++++++++++++++++++++++++++++++ cv.md | 109 ++++++++++++++++++++++++++ description.md | 77 +++++++++++++++++++ graduation.md | 73 ++++++++++++++++++ 5 files changed, 636 insertions(+) create mode 100644 README.md create mode 100644 ReleaseNotes.md create mode 100644 cv.md create mode 100644 description.md create mode 100644 graduation.md diff --git a/README.md b/README.md new file mode 100644 index 000000000000..4183a09d949b --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +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: +- должны возвращаться только записи между startTime и endTime +- поле UserMealWithExceed.exceed должно показывать, + превышает ли сумма калорий за весь день параметра метода caloriesPerDay + +Т.е UserMealWithExceed - это запись одной еды, но поле exceeded будет одинаково для всех записей за этот день. + +- Проверьте результат выполнения ДЗ (можно проверить логику в http://topjava.herokuapp.com , список еды) +- Оцените Time complexity вашего алгоритма, если он O(N*N)- попробуйте сделать O(N). +``` +- Java 8 Date and Time API +- Алгоритмы и структуры данных для начинающих: сложность алгоритмов +- Time complexity +- Временная сложность алгоритма +- Вычислительная сложность + +#### Optional (Java 8 Stream API) +``` +Сделать реализацию через Java 8 Stream API. +``` +- Видео: Доступно о Java 8 Lambda +- Java 8: Lambda выражения +- Java 8: Потоки +- Pуководство по Java 8 Stream +- [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/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/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 или через кэширование)? From 77f5d45abb946d71ae30f760907d346f5c7aa027 Mon Sep 17 00:00:00 2001 From: JavaOPs Date: Thu, 28 Mar 2019 17:23:13 +0300 Subject: [PATCH 02/97] Add sample lesson07 Update read.me --- README.md | 9 ++- doc/lesson07.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 doc/lesson07.md diff --git a/README.md b/README.md index 4183a09d949b..d6e1d486e3b9 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Java Enterprise Online Project ## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW0 ``` -Реализовать метод UserMealsUtil.getFilteredWithExceeded: +Реализовать метод UserMealsUtil.getFilteredWithExceeded через циклы (`forEach`): - должны возвращаться только записи между startTime и endTime - поле UserMealWithExceed.exceed должно показывать, превышает ли сумма калорий за весь день параметра метода caloriesPerDay @@ -73,7 +73,7 @@ Java Enterprise Online Project Т.е UserMealWithExceed - это запись одной еды, но поле exceeded будет одинаково для всех записей за этот день. - Проверьте результат выполнения ДЗ (можно проверить логику в http://topjava.herokuapp.com , список еды) -- Оцените Time complexity вашего алгоритма, если он O(N*N)- попробуйте сделать O(N). +- Оцените Time complexity алгоритма. Если она больше O(N), например O(N*N) или N*log(N), сделайте O(N). ``` - Java 8 Date and Time API - Алгоритмы и структуры данных для начинающих: сложность алгоритмов @@ -81,6 +81,8 @@ Java Enterprise Online Project - Временная сложность алгоритма - Вычислительная сложность +#### ВНИМАНИЕ: варианты Optional делайте в `UserMealsUtil` в одной ветке в разных методах. Проще делать, проще проверять + #### Optional (Java 8 Stream API) ``` Сделать реализацию через Java 8 Stream API. @@ -88,7 +90,8 @@ Java Enterprise Online Project - Видео: Доступно о Java 8 Lambda - Java 8: Lambda выражения - Java 8: Потоки -- Pуководство по Java 8 Stream +- Pуководство по Java 8 Stream +- Java 8 Stream API в картинках и примерах - [7 способов использовать groupingBy в Stream API](https://habrahabr.ru/post/348536) - Лямбда-выражения в Java 8 - A Guide to Java 8 diff --git a/doc/lesson07.md b/doc/lesson07.md new file mode 100644 index 000000000000..7326f915dc06 --- /dev/null +++ b/doc/lesson07.md @@ -0,0 +1,174 @@ +# [Онлайн стажировка Spring 5/JPA Enterprise (Topjava)](http://javaops.ru/view/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(",")`: +``` +... +``` + +## Занятие 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://www.youtube.com/watch?v=YmLzT-j1hU4) +#### 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) + +- [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://youtu.be/33_2yOck4ak) +#### 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/file/d/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/file/d/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/file/d/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) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Тестирование через SoapUi. UTF-8 +#### Apply 7_13_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}` ? + +Так делают, только при отношении агрегация. В случае критериев, поиска или страничных данных они передаются как параметр. Смотри также: +- [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: При проблемах с собственным форматтером убедитесь, что в конфигурации ` Date: Thu, 28 Mar 2019 22:40:12 +0300 Subject: [PATCH 03/97] Update lesson07 --- README.md | 1 + doc/lesson07.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/README.md b/README.md index d6e1d486e3b9..99c5d0835be0 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Java Enterprise Online Project - 12: Обращайте внимание на комментарии к вашим коммитам в git. Они должны быть короткие и информативные (лучше на english) - 13: Не полагайтесь в решении на то, что список будет подаваться отсортированным. Такого условия нет. ----- +## [Пример 7-го занятия онлайн стажировки, несколько видео открыто](https://github.com/JavaOPs/topjava/blob/master/doc/lesson07.md) ### Полезные ресурсы > ВНИМАНИЕ: diff --git a/doc/lesson07.md b/doc/lesson07.md index 7326f915dc06..2327d2e6bb53 100644 --- a/doc/lesson07.md +++ b/doc/lesson07.md @@ -1,4 +1,6 @@ # [Онлайн стажировка Spring 5/JPA Enterprise (Topjava)](http://javaops.ru/view/topjava) +# REST, REST контроллеры, тестирование Spring MVC контроллеров +## Седьмое занятие, несколько видео открыто. [Формат обучения на стажировке](http://javaops.ru/#format) ## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW6 From f61784bac98b7d3c5342a0572eb81c5acd89e83d Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Fri, 29 Mar 2019 02:35:27 +0300 Subject: [PATCH 04/97] Update lesson07.md --- doc/lesson07.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/lesson07.md b/doc/lesson07.md index 2327d2e6bb53..9a59e0fda3d4 100644 --- a/doc/lesson07.md +++ b/doc/lesson07.md @@ -1,6 +1,11 @@ # [Онлайн стажировка Spring 5/JPA Enterprise (Topjava)](http://javaops.ru/view/topjava) -# REST, REST контроллеры, тестирование Spring MVC контроллеров -## Седьмое занятие, несколько видео открыто. [Формат обучения на стажировке](http://javaops.ru/#format) +## REST, REST контроллеры, тестирование Spring MVC контроллеров (несколько видео открыто) +## [Формат обучения на стажировке](http://javaops.ru/#format) +- Не стоит стремиться прочитать все ссылки урока, их можно использовать как справочник. Гораздо важнее пройти основной материал урока и сделать Домашнее Задание +- Обязательно посмотри правила работы с патчами на проекте +- Делать Apply Patch лучше по одному, непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (`Version Control->Local Changes-> Ctrl+D`) +- При первом Apply удобнее выбрать имя локального ченджлиста Name: Default. Далее все остальные патчи также будут в него попадать. +- Код проекта обновляется и не всегда совпадает с видео (можно увидеть как развивался проект). Изменения в проекте указываю после соответствующего патча. ## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW6 From adc2b0caa72ce92de851deccb22dabed162387a2 Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Tue, 9 Apr 2019 23:50:08 +0300 Subject: [PATCH 05/97] Update ReleaseNotes.md --- ReleaseNotes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 5298ae94db76..07d5f1370c8d 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,4 +1,9 @@ # TopJava Release Notes +### Topjava 16 +- Выделил общий код реализации хранения в памяти в `InMemoryBaseRepositoryImpl` +- Сделал подтверждение для удаления записей +- Валидации дублирования записей сделал через `WebDataBinder` и `Validator` + ### Topjava 15 - Миграция на Servlet API 4.0 / Tomcat 9.x - [Миграция на JDK11](http://javaops.ru/view/resources/jdk8_11) From c61fe197d9c3c4e226fa7555818240c2a2d446dd Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Mon, 15 Apr 2019 02:46:47 +0300 Subject: [PATCH 06/97] Update ReleaseNotes.md --- ReleaseNotes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 07d5f1370c8d..403a93e7146d 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -2,7 +2,7 @@ ### Topjava 16 - Выделил общий код реализации хранения в памяти в `InMemoryBaseRepositoryImpl` - Сделал подтверждение для удаления записей -- Валидации дублирования записей сделал через `WebDataBinder` и `Validator` +- Валидации дублирования записей в этом выпуске будем делать через `WebDataBinder` и `Validator` ### Topjava 15 - Миграция на Servlet API 4.0 / Tomcat 9.x From adf5fc506663858678d74ded144010f2a5e075b9 Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Mon, 15 Apr 2019 02:47:35 +0300 Subject: [PATCH 07/97] Update ReleaseNotes.md --- ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 403a93e7146d..7b60e3fcdbc3 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -3,6 +3,7 @@ - Выделил общий код реализации хранения в памяти в `InMemoryBaseRepositoryImpl` - Сделал подтверждение для удаления записей - Валидации дублирования записей в этом выпуске будем делать через `WebDataBinder` и `Validator` +- Отдельно сделаем патч на адаптивный дизайн UI. ### Topjava 15 - Миграция на Servlet API 4.0 / Tomcat 9.x From cdf82e27206c394aa4ac4b21b1de9f324bf7baef Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Mon, 15 Apr 2019 02:50:06 +0300 Subject: [PATCH 08/97] Update ReleaseNotes.md --- ReleaseNotes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 7b60e3fcdbc3..720374469301 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -2,6 +2,7 @@ ### Topjava 16 - Выделил общий код реализации хранения в памяти в `InMemoryBaseRepositoryImpl` - Сделал подтверждение для удаления записей +- Обновились видео 7-го занятия. [Выложил его как пример занятия, некоторые видео открыты](https://github.com/JavaOPs/topjava/blob/master/doc/lesson07.md) - Валидации дублирования записей в этом выпуске будем делать через `WebDataBinder` и `Validator` - Отдельно сделаем патч на адаптивный дизайн UI. From 13edb889a5f3673b048555434830853dc4079d9a Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Thu, 25 Apr 2019 02:51:48 +0300 Subject: [PATCH 09/97] Update ReleaseNotes.md --- ReleaseNotes.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 720374469301..ae142c42357e 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -3,8 +3,7 @@ - Выделил общий код реализации хранения в памяти в `InMemoryBaseRepositoryImpl` - Сделал подтверждение для удаления записей - Обновились видео 7-го занятия. [Выложил его как пример занятия, некоторые видео открыты](https://github.com/JavaOPs/topjava/blob/master/doc/lesson07.md) -- Валидации дублирования записей в этом выпуске будем делать через `WebDataBinder` и `Validator` -- Отдельно сделаем патч на адаптивный дизайн UI. +- Сделали валидации дублирования email через `WebDataBinder` и `Validator` ### Topjava 15 - Миграция на Servlet API 4.0 / Tomcat 9.x From 64217e542a106e388a9c39417dccdd681e302f94 Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Thu, 9 May 2019 23:13:43 +0300 Subject: [PATCH 10/97] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99c5d0835be0..6d46c4df57e9 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Java Enterprise Online Project ### Полезные ресурсы > ВНИМАНИЕ: -> - ДЗ первого урока будет связано с созданием небольшого CRUD приложения (в памяти, без DB) на JSP и сервлетах. Введение будет, но предварительное знакомство не помешает. +> - **ДЗ первого урока будет связано с [созданием небольшого CRUD приложения (в памяти, без DB) на JSP и сервлетах](https://danielniko.wordpress.com/2012/04/17/simple-crud-using-jsp-servlet-and-mysql/)**. Введение будет, но предварительное знакомство не помешает. > - основы JavaSсript необходимы для понимания проекта, начиная с 8-го занятия! Все остальное - опционально. From 7ba51b04acc8625e175ab70678b2911ba114c6dc Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Thu, 9 May 2019 23:14:17 +0300 Subject: [PATCH 11/97] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d46c4df57e9..de83d87b4911 100644 --- a/README.md +++ b/README.md @@ -174,8 +174,8 @@ Java Enterprise Online Project - A Guide to Java 8 ### Туториалы, разное -[Что нужно знать о бэкенде новичку в веб-разработке](https://tproger.ru/translations/backend-web-development) -[Туториалы: Spring Framework, Hibernate, Java Core, JDBC](http://proselyte.net/tutorials/) +- [Что нужно знать о бэкенде новичку в веб-разработке](https://tproger.ru/translations/backend-web-development) +- [Туториалы: Spring Framework, Hibernate, Java Core, JDBC](http://proselyte.net/tutorials/) #### Сервлеты - Как создать Servlet? Полное руководство. From 4a857f2aa8e92a7d6204c337e76e32ff033dd3b4 Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Wed, 22 May 2019 18:02:25 +0300 Subject: [PATCH 12/97] Update graduation.md --- graduation.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/graduation.md b/graduation.md index f167155ab997..2541d69cd9a1 100644 --- a/graduation.md +++ b/graduation.md @@ -67,7 +67,9 @@ _Антуан де Сент-Экзюпери_ - 23: Если используете кэширование, **тщательно продумайте, что надо кэшировать (самые частые запросы)**, а что нет (большие или редкозапрашиваемые данные)! ## Попробуйте подергать свое API по всем типичным сценариям ТЗ! -- Удобно использовать? Можно сделать проще? Например чтобы проголосовать за ресторан залогиненному юзеру достаточно `restorauntId`. -- Удовлетворяет ли принципам REST (см. ссылки выше)? +- Удобно использовать? Можно сделать проще? *Например чтобы проголосовать за ресторан залогиненному юзеру достаточно `restorauntId`.* - Сколько раз пришлось его вызвать API для типичного сценария (нарпимер посмотреть рестораны с едой)? - Сколько запросов к базе было сделано? Можно ли сократить (например с FETCH/Graph или через кэширование)? +- **API ДОЛЖНО соответствовать принципам REST (см. ссылки выше)** +- **ОБЯЗАТЕЛЬНО: запустите `mvn test`- ошибок быть не должно*** +- **ОБЯЗАТЕЛЬНО: запустите приложение без всяких предварительных настроек (базы, переменных окружения, ..), лучше на другом компьютере. Должно запускаться!** From 046e67b010aaabd870a01a55615f52a36e34bc92 Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Wed, 22 May 2019 18:02:59 +0300 Subject: [PATCH 13/97] Update graduation.md From 562d53dc2411bf758faf5b0208540839cb24f028 Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Wed, 22 May 2019 18:04:31 +0300 Subject: [PATCH 14/97] Update graduation.md --- graduation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graduation.md b/graduation.md index 2541d69cd9a1..6aee3f6ed695 100644 --- a/graduation.md +++ b/graduation.md @@ -71,5 +71,5 @@ _Антуан де Сент-Экзюпери_ - Сколько раз пришлось его вызвать API для типичного сценария (нарпимер посмотреть рестораны с едой)? - Сколько запросов к базе было сделано? Можно ли сократить (например с FETCH/Graph или через кэширование)? - **API ДОЛЖНО соответствовать принципам REST (см. ссылки выше)** -- **ОБЯЗАТЕЛЬНО: запустите `mvn test`- ошибок быть не должно*** +- **ОБЯЗАТЕЛЬНО: запустите `mvn test`- ошибок быть не должно** - **ОБЯЗАТЕЛЬНО: запустите приложение без всяких предварительных настроек (базы, переменных окружения, ..), лучше на другом компьютере. Должно запускаться!** From 378183e8c7a1557e2b1c2962c5efd7a9e7cc4202 Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Sat, 15 Jun 2019 13:14:26 +0300 Subject: [PATCH 15/97] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de83d87b4911..30b9de42d7e0 100644 --- a/README.md +++ b/README.md @@ -179,9 +179,10 @@ Java Enterprise Online Project #### Сервлеты - Как создать Servlet? Полное руководство. +- [Сервлеты](https://metanit.com/java/javaee/4.1.php) #### JDBC, SQL -- Основы SQL на примере задачи +- Основы SQL на примере задачи - Уроки по JDBC - Learn SQL - Интуит. Основы SQL From 85ca3db59332d526450325c399a4ca571bd77c9c Mon Sep 17 00:00:00 2001 From: Java Online Projects Date: Thu, 15 Aug 2019 04:23:44 +0300 Subject: [PATCH 16/97] Update ReleaseNotes.md --- ReleaseNotes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index ae142c42357e..65a11b9c5afc 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,4 +1,10 @@ # TopJava Release Notes +### Topjava 17 +- Удалил `Impl` из названий репозиториев +- Удалил интерфейсы к сервисам, использую классы +- Добавил `AdminRestController.enable`, вызов через PATCH метод +- Добавил валидацию для jdbc через Bean Validation API + ### Topjava 16 - Выделил общий код реализации хранения в памяти в `InMemoryBaseRepositoryImpl` - Сделал подтверждение для удаления записей From 8cde73f37996c8b45037c806ff8abb171f747f9e Mon Sep 17 00:00:00 2001 From: Alexey Konyagin Date: Tue, 20 Aug 2019 12:36:11 +0300 Subject: [PATCH 17/97] Prepare to HW0 --- pom.xml | 2 +- .../java/ru/javawebinar/topjava/Main.java | 7 ++--- .../javawebinar/topjava/model/UserMeal.java | 29 +++++++++++++++++ .../topjava/model/UserMealWithExceed.java | 20 ++++++++++++ .../ru/javawebinar/topjava/util/TimeUtil.java | 9 ++++++ .../topjava/util/UserMealsUtil.java | 31 +++++++++++++++++++ 6 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/model/UserMeal.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/TimeUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java diff --git a/pom.xml b/pom.xml index c8a1c78f3b29..e3b8143e3e63 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 + 3.7.0 ${java.version} ${java.version} diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java index b23a2f0961fc..cb7e35af6afa 100644 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ b/src/main/java/ru/javawebinar/topjava/Main.java @@ -1,11 +1,8 @@ package ru.javawebinar.topjava; /** - * User: gkislin - * Date: 05.08.2015 - * - * @link http://caloriesmng.herokuapp.com/ - * @link https://github.com/JavaOPs/topjava + * @see Demo + * @see Initial project */ public class Main { public static void main(String[] args) { diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java b/src/main/java/ru/javawebinar/topjava/model/UserMeal.java new file mode 100644 index 000000000000..d8f91b127f6a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/UserMeal.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.model; + +import java.time.LocalDateTime; + +public class UserMeal { + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + public UserMeal(LocalDateTime dateTime, String description, int calories) { + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java b/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java new file mode 100644 index 000000000000..3fe62373ac11 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.model; + +import java.time.LocalDateTime; + +public class UserMealWithExceed { + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + private final boolean exceed; + + public UserMealWithExceed(LocalDateTime dateTime, String description, int calories, boolean exceed) { + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + this.exceed = exceed; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java new file mode 100644 index 000000000000..b7eb2af6f93e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java @@ -0,0 +1,9 @@ +package ru.javawebinar.topjava.util; + +import java.time.LocalTime; + +public class TimeUtil { + public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { + return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java new file mode 100644 index 000000000000..6ba2f4e47935 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java @@ -0,0 +1,31 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.model.UserMealWithExceed; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +public class UserMealsUtil { + public static void main(String[] args) { + List mealList = Arrays.asList( + new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,10,0), "Завтрак", 500), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,13,0), "Обед", 1000), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,20,0), "Ужин", 500), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,10,0), "Завтрак", 1000), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,13,0), "Обед", 500), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,20,0), "Ужин", 510) + ); + getFilteredWithExceeded(mealList, LocalTime.of(7, 0), LocalTime.of(12,0), 2000); +// .toLocalDate(); +// .toLocalTime(); + } + + public static List getFilteredWithExceeded(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + // TODO return filtered list with correctly exceeded field + return null; + } +} From 414765e649496e372450d45d57cc715d53bf9ed0 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:30:09 +0300 Subject: [PATCH 18/97] Applied patch - 1_0_fix --- .gitignore | 2 +- pom.xml | 2 +- .../model/{UserMeal.java => Meal.java} | 4 +-- .../{UserMealWithExceed.java => MealTo.java} | 8 ++--- .../javawebinar/topjava/util/MealsUtil.java | 31 +++++++++++++++++++ .../topjava/util/UserMealsUtil.java | 31 ------------------- 6 files changed, 39 insertions(+), 39 deletions(-) rename src/main/java/ru/javawebinar/topjava/model/{UserMeal.java => Meal.java} (83%) rename src/main/java/ru/javawebinar/topjava/model/{UserMealWithExceed.java => MealTo.java} (59%) create mode 100644 src/main/java/ru/javawebinar/topjava/util/MealsUtil.java delete mode 100644 src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java diff --git a/.gitignore b/.gitignore index 6ca05c4a5184..73e6d9e3dd11 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ out target *.iml log - +*.patch diff --git a/pom.xml b/pom.xml index e3b8143e3e63..0b1c2896da5b 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.8.1 ${java.version} ${java.version} diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java similarity index 83% rename from src/main/java/ru/javawebinar/topjava/model/UserMeal.java rename to src/main/java/ru/javawebinar/topjava/model/Meal.java index d8f91b127f6a..f546cef0f74a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -2,14 +2,14 @@ import java.time.LocalDateTime; -public class UserMeal { +public class Meal { private final LocalDateTime dateTime; private final String description; private final int calories; - public UserMeal(LocalDateTime dateTime, String description, int calories) { + public Meal(LocalDateTime dateTime, String description, int calories) { this.dateTime = dateTime; this.description = description; this.calories = calories; diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java similarity index 59% rename from src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java rename to src/main/java/ru/javawebinar/topjava/model/MealTo.java index 3fe62373ac11..743053b13aa8 100644 --- a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealTo.java @@ -2,19 +2,19 @@ import java.time.LocalDateTime; -public class UserMealWithExceed { +public class MealTo { private final LocalDateTime dateTime; private final String description; private final int calories; - private final boolean exceed; + private final boolean excess; - public UserMealWithExceed(LocalDateTime dateTime, String description, int calories, boolean exceed) { + public MealTo(LocalDateTime dateTime, String description, int calories, boolean excess) { this.dateTime = dateTime; this.description = description; this.calories = calories; - this.exceed = exceed; + this.excess = excess; } } 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..5f38228bf35f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -0,0 +1,31 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.MealTo; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +public class MealsUtil { + public static void main(String[] args) { + List meals = Arrays.asList( + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) + ); + getFilteredWithExcess(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); +// .toLocalDate(); +// .toLocalTime(); + } + + public static List getFilteredWithExcess(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + // TODO return filtered list with correctly excess field + return null; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java deleted file mode 100644 index 6ba2f4e47935..000000000000 --- a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java +++ /dev/null @@ -1,31 +0,0 @@ -package ru.javawebinar.topjava.util; - -import ru.javawebinar.topjava.model.UserMeal; -import ru.javawebinar.topjava.model.UserMealWithExceed; - -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.Month; -import java.util.Arrays; -import java.util.List; - -public class UserMealsUtil { - public static void main(String[] args) { - List mealList = Arrays.asList( - new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,10,0), "Завтрак", 500), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,13,0), "Обед", 1000), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,20,0), "Ужин", 500), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,10,0), "Завтрак", 1000), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,13,0), "Обед", 500), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,20,0), "Ужин", 510) - ); - getFilteredWithExceeded(mealList, LocalTime.of(7, 0), LocalTime.of(12,0), 2000); -// .toLocalDate(); -// .toLocalTime(); - } - - public static List getFilteredWithExceeded(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - // TODO return filtered list with correctly exceeded field - return null; - } -} From 345edcfc5f1e41feca1961943e5c92c81c0e6d84 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:30:33 +0300 Subject: [PATCH 19/97] Applied patch - 1_1_HW0_stream --- .../ru/javawebinar/topjava/model/Meal.java | 10 +++++++++ .../ru/javawebinar/topjava/model/MealTo.java | 12 +++++++++- .../javawebinar/topjava/util/MealsUtil.java | 22 ++++++++++++++----- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index f546cef0f74a..943ff5cd59fa 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.model; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; public class Meal { private final LocalDateTime dateTime; @@ -26,4 +28,12 @@ public String getDescription() { public int getCalories() { return calories; } + + public LocalDate getDate() { + return dateTime.toLocalDate(); + } + + public LocalTime getTime() { + return dateTime.toLocalTime(); + } } diff --git a/src/main/java/ru/javawebinar/topjava/model/MealTo.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java index 743053b13aa8..23f88cee8915 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealTo.java @@ -17,4 +17,14 @@ public MealTo(LocalDateTime dateTime, String description, int calories, boolean this.calories = calories; this.excess = excess; } -} + + @Override + public String toString() { + return "MealTo{" + + "dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + ", excess=" + excess + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 5f38228bf35f..78960112d994 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -3,11 +3,14 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.model.MealTo; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; public class MealsUtil { public static void main(String[] args) { @@ -19,13 +22,22 @@ public static void main(String[] args) { new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) ); - getFilteredWithExcess(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); -// .toLocalDate(); -// .toLocalTime(); + List mealsWithExcess = getFilteredWithExcess(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); + mealsWithExcess.forEach(System.out::println); } public static List getFilteredWithExcess(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - // TODO return filtered list with correctly excess field - return null; + Map caloriesSumByDate = meals.stream() + .collect( + Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) +// Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum) + ); + + return meals.stream() + .filter(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime)) + .map(meal -> + new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), + caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .collect(Collectors.toList()); } } \ No newline at end of file From ee3653104db8682ff9f58113cd97a1df56ae4bc8 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:30:51 +0300 Subject: [PATCH 20/97] Applied patch - 1_2_HW0_cycle --- .../javawebinar/topjava/util/MealsUtil.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 78960112d994..34b26203c59f 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -7,9 +7,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; public class MealsUtil { @@ -24,6 +22,8 @@ public static void main(String[] args) { ); List mealsWithExcess = getFilteredWithExcess(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); mealsWithExcess.forEach(System.out::println); + + System.out.println(getFilteredWithExcessByCycle(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); } public static List getFilteredWithExcess(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { @@ -35,9 +35,25 @@ public static List getFilteredWithExcess(List meals, LocalTime sta return meals.stream() .filter(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime)) - .map(meal -> - new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), - caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .map(meal -> createWithExcess(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) .collect(Collectors.toList()); } + + public static List getFilteredWithExcessByCycle(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + + final Map caloriesSumByDate = new HashMap<>(); + meals.forEach(meal -> caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum)); + + final List mealsWithExcess = new ArrayList<>(); + meals.forEach(meal -> { + if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { + mealsWithExcess.add(createWithExcess(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)); + } + }); + return mealsWithExcess; + } + + private static MealTo createWithExcess(Meal meal, boolean excess) { + return new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); + } } \ No newline at end of file From b82a6d8e0953d7a7992c264e5ab3a96f18392ecb Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:31:19 +0300 Subject: [PATCH 21/97] Applied patch - 1_3_HW0_optional2 --- .../ru/javawebinar/topjava/model/MealTo.java | 6 + .../javawebinar/topjava/util/MealsUtil.java | 158 ++++++++++++++++-- 2 files changed, 151 insertions(+), 13 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/MealTo.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java index 23f88cee8915..69178a78b8e6 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealTo.java @@ -9,6 +9,8 @@ public class MealTo { private final int calories; +// private final Supplier excess; +// private final AtomicBoolean excess; private final boolean excess; public MealTo(LocalDateTime dateTime, String description, int calories, boolean excess) { @@ -18,6 +20,10 @@ public MealTo(LocalDateTime dateTime, String description, int calories, boolean this.excess = excess; } +// public Boolean getExcess() { +// return excess.get(); +// } + @Override public String toString() { return "MealTo{" + diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 34b26203c59f..2e8d76fc2ad5 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -8,10 +8,18 @@ import java.time.LocalTime; import java.time.Month; import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collector; import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toList; public class MealsUtil { - public static void main(String[] args) { + private static final int DEFAULT_CALORIES_PER_DAY = 2000; + + public static void main(String[] args) throws ExecutionException, InterruptedException { List meals = Arrays.asList( new Meal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), new Meal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), @@ -20,40 +28,164 @@ public static void main(String[] args) { new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) ); - List mealsWithExcess = getFilteredWithExcess(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); - mealsWithExcess.forEach(System.out::println); + LocalTime startTime = LocalTime.of(7, 0); + LocalTime endTime = LocalTime.of(12, 0); + + List mealsTo = getFiltered(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY); + mealsTo.forEach(System.out::println); - System.out.println(getFilteredWithExcessByCycle(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); + System.out.println(getFilteredByCycle(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); + System.out.println(getFilteredByRecursion(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); +// System.out.println(getFilteredByAtomic(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); +// System.out.println(getFilteredByClosure(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); + System.out.println(getFilteredByExecutor(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); + System.out.println(getFilteredByFlatMap(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); + System.out.println(getFilteredByCollector(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); } - public static List getFilteredWithExcess(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + public static List getFiltered(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { Map caloriesSumByDate = meals.stream() .collect( Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) // Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum) ); - return meals.stream() .filter(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime)) - .map(meal -> createWithExcess(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) .collect(Collectors.toList()); } - public static List getFilteredWithExcessByCycle(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - + private static List getFilteredByCycle(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { final Map caloriesSumByDate = new HashMap<>(); meals.forEach(meal -> caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum)); - final List mealsWithExcess = new ArrayList<>(); + final List mealsTo = new ArrayList<>(); + meals.forEach(meal -> { + if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { + mealsTo.add(createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)); + } + }); + return mealsTo; + } + + private static List getFilteredByRecursion(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + ArrayList result = new ArrayList<>(); + filterWithRecursion(new LinkedList<>(meals), startTime, endTime, caloriesPerDay, new HashMap<>(), result); + return result; + } + + private static void filterWithRecursion(LinkedList meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay, + Map dailyCaloriesMap, List result) { + if (meals.isEmpty()) return; + + Meal meal = meals.pop(); + dailyCaloriesMap.merge(meal.getDate(), meal.getCalories(), Integer::sum); + filterWithRecursion(meals, startTime, endTime, caloriesPerDay, dailyCaloriesMap, result); + if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { + result.add(createTo(meal, dailyCaloriesMap.get(meal.getDate()) > caloriesPerDay)); + } + } + +/* + private static List getFilteredByAtomic(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + Map caloriesSumByDate = new HashMap<>(); + Map exceededMap = new HashMap<>(); + + List mealsTo = new ArrayList<>(); + meals.forEach(meal -> { + AtomicBoolean wrapBoolean = exceededMap.computeIfAbsent(meal.getDate(), date -> new AtomicBoolean()); + Integer dailyCalories = caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum); + if (dailyCalories > caloriesPerDay) { + wrapBoolean.set(true); + } + if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { + mealsTo.add(createTo(meal, wrapBoolean)); // also change createWithExcess and MealTo.excess + } + }); + return mealsTo; + } + + private static List getFilteredByClosure(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + final Map caloriesSumByDate = new HashMap<>(); + List mealsTo = new ArrayList<>(); + mealList.forEach(meal -> { + caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum); + if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { + mealsTo.add(createTo(meal, () -> (caloriesSumByDate.get(meal.getDate()) > caloriesPerDay))); // also change createWithExcess and MealTo.excess + } + } + ); + return mealsTo; + } +*/ + + private static List getFilteredByExecutor(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) throws InterruptedException, ExecutionException { + Map caloriesSumByDate = new HashMap<>(); + List> tasks = new ArrayList<>(); + meals.forEach(meal -> { + caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum); if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { - mealsWithExcess.add(createWithExcess(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)); + tasks.add(() -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)); } }); - return mealsWithExcess; + ExecutorService executorService = Executors.newFixedThreadPool(4); + List> futures = executorService.invokeAll(tasks); + executorService.shutdown(); + final List mealsTo = new ArrayList<>(); + for (Future future : futures) { + mealsTo.add(future.get()); + } + return mealsTo; + } + + private static List getFilteredByFlatMap(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + Collection> list = meals.stream() + .collect(Collectors.groupingBy(Meal::getDate)).values(); + + return list.stream() + .flatMap(dayMeals -> { + boolean excess = dayMeals.stream().mapToInt(Meal::getCalories).sum() > caloriesPerDay; + return dayMeals.stream().filter(meal -> + TimeUtil.isBetween(meal.getTime(), startTime, endTime)) + .map(meal -> createTo(meal, excess)); + }).collect(toList()); + } + + private static List getFilteredByCollector(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + final class Aggregate { + private final List dailyMeals = new ArrayList<>(); + private int dailySumOfCalories; + + private void accumulate(Meal meal) { + dailySumOfCalories += meal.getCalories(); + if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { + dailyMeals.add(meal); + } + } + + // never invoked if the upstream is sequential + private Aggregate combine(Aggregate that) { + this.dailySumOfCalories += that.dailySumOfCalories; + this.dailyMeals.addAll(that.dailyMeals); + return this; + } + + private Stream finisher() { + final boolean excess = dailySumOfCalories > caloriesPerDay; + return dailyMeals.stream().map(meal -> createTo(meal, excess)); + } + } + + Collection> values = meals.stream() + .collect(Collectors.groupingBy(Meal::getDate, + Collector.of(Aggregate::new, Aggregate::accumulate, Aggregate::combine, Aggregate::finisher)) + ).values(); + + return values.stream().flatMap(identity()).collect(toList()); } - private static MealTo createWithExcess(Meal meal, boolean excess) { + private static MealTo createTo(Meal meal, boolean excess) { return new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); } } \ No newline at end of file From d0b719839095232b35372bd2854f7ee62d8eb3a0 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:32:06 +0300 Subject: [PATCH 22/97] Applied patch - 1_4_switch_to_war --- pom.xml | 2 +- src/main/webapp/WEB-INF/web.xml | 19 +++++++++++++++++++ src/main/webapp/index.html | 13 +++++++++++++ src/main/webapp/users.jsp | 11 +++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/index.html create mode 100644 src/main/webapp/users.jsp diff --git a/pom.xml b/pom.xml index 0b1c2896da5b..b4426c1aa2d6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ru.javawebinar topjava - jar + war 1.0-SNAPSHOT diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..c63810c43593 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,19 @@ + + + Topjava + + + userServlet + ru.javawebinar.topjava.web.UserServlet + 0 + + + userServlet + /users + + + diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html new file mode 100644 index 000000000000..6253517f8b84 --- /dev/null +++ b/src/main/webapp/index.html @@ -0,0 +1,13 @@ + + + + Java Enterprise (Topjava) + + +

Проект Java Enterprise (Topjava)

+
+ + + diff --git a/src/main/webapp/users.jsp b/src/main/webapp/users.jsp new file mode 100644 index 000000000000..60bdc9b83192 --- /dev/null +++ b/src/main/webapp/users.jsp @@ -0,0 +1,11 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + Users + + +

Home

+
+

Users

+ + \ No newline at end of file From d422e17e80d5a0bca776cdb1c26b8b1d04523586 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:32:26 +0300 Subject: [PATCH 23/97] Applied patch - 1_5_add_servlet_api --- pom.xml | 7 +++++++ .../ru/javawebinar/topjava/web/UserServlet.java | 15 +++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/web/UserServlet.java diff --git a/pom.xml b/pom.xml index b4426c1aa2d6..3cd703f93681 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,13 @@ + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java new file mode 100644 index 000000000000..76056e06c019 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -0,0 +1,15 @@ +package ru.javawebinar.topjava.web; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class UserServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + request.getRequestDispatcher("/users.jsp").forward(request, response); + } +} From f1b8bc2904f0e705a6a5a3fa526dd35095da8d4f Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:32:47 +0300 Subject: [PATCH 24/97] Applied patch - 1_6_forward_to_redirect --- pom.xml | 2 +- src/main/java/ru/javawebinar/topjava/web/UserServlet.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3cd703f93681..f7abfea2b739 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ topjava - install + package org.apache.maven.plugins diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index 76056e06c019..11f282bac482 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -10,6 +10,7 @@ public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - request.getRequestDispatcher("/users.jsp").forward(request, response); +// request.getRequestDispatcher("/users.jsp").forward(request, response); + response.sendRedirect("users.jsp"); } } From d152d73fa8b268abb6f85d6afaa11d936c17370c Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:33:06 +0300 Subject: [PATCH 25/97] Applied patch - 1_7_logging --- pom.xml | 26 +++++++++++++++++ .../javawebinar/topjava/web/UserServlet.java | 7 +++++ src/main/resources/logback.xml | 29 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/main/resources/logback.xml diff --git a/pom.xml b/pom.xml index f7abfea2b739..4dbe953d10bb 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,10 @@ 1.8 UTF-8 UTF-8 + + + 1.2.3 + 1.7.28 @@ -34,6 +38,28 @@ + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + runtime + + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index 11f282bac482..ef52d67576c0 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -1,15 +1,22 @@ package ru.javawebinar.topjava.web; +import org.slf4j.Logger; + import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import static org.slf4j.LoggerFactory.getLogger; + public class UserServlet extends HttpServlet { + private static final Logger log = getLogger(UserServlet.class); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + log.debug("redirect to users"); + // request.getRequestDispatcher("/users.jsp").forward(request, response); response.sendRedirect("users.jsp"); } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 000000000000..e9b900b26669 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,29 @@ + + + + + + + + ${TOPJAVA_ROOT}/log/topjava.log + + + UTF-8 + %date %-5level %logger{0} [%file:%line] %msg%n + + + + + + UTF-8 + %-5level %logger{0} [%file:%line] %msg%n + + + + + + + + + + From 41d189ad76716793b48c5230af5a717ddba5331c Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:33:42 +0300 Subject: [PATCH 26/97] Applied patch - 2_1_HW1 --- pom.xml | 6 + .../java/ru/javawebinar/topjava/Main.java | 11 -- .../ru/javawebinar/topjava/model/MealTo.java | 20 +- .../topjava/util/DateTimeUtil.java | 18 ++ .../javawebinar/topjava/util/MealsUtil.java | 180 ++---------------- .../ru/javawebinar/topjava/util/TimeUtil.java | 9 - .../javawebinar/topjava/web/MealServlet.java | 22 +++ .../javawebinar/topjava/web/UserServlet.java | 6 +- src/main/webapp/WEB-INF/tld/functions.tld | 16 ++ src/main/webapp/WEB-INF/web.xml | 10 + src/main/webapp/index.html | 1 + src/main/webapp/meals.jsp | 48 +++++ 12 files changed, 158 insertions(+), 189 deletions(-) delete mode 100644 src/main/java/ru/javawebinar/topjava/Main.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java delete mode 100644 src/main/java/ru/javawebinar/topjava/util/TimeUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/MealServlet.java create mode 100644 src/main/webapp/WEB-INF/tld/functions.tld create mode 100644 src/main/webapp/meals.jsp diff --git a/pom.xml b/pom.xml index 4dbe953d10bb..62fe2c95d1d8 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,12 @@ 4.0.1 provided + + + javax.servlet + jstl + 1.2 + 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 cb7e35af6afa..000000000000 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.javawebinar.topjava; - -/** - * @see Demo - * @see Initial project - */ -public class Main { - public static void main(String[] args) { - System.out.format("Hello Topjava Enterprise!"); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/model/MealTo.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java index 69178a78b8e6..697ad5ef99bf 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealTo.java @@ -9,8 +9,6 @@ public class MealTo { private final int calories; -// private final Supplier excess; -// private final AtomicBoolean excess; private final boolean excess; public MealTo(LocalDateTime dateTime, String description, int calories, boolean excess) { @@ -20,9 +18,21 @@ public MealTo(LocalDateTime dateTime, String description, int calories, boolean this.excess = excess; } -// public Boolean getExcess() { -// return excess.get(); -// } + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public boolean isExcess() { + return excess; + } @Override public String toString() { 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..fc9e659fdca4 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -0,0 +1,18 @@ +package ru.javawebinar.topjava.util; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public class DateTimeUtil { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { + return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; + } + + public static String toString(LocalDateTime ldt) { + return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); + } +} + diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 2e8d76fc2ad5..a7f92b9d1796 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -7,184 +7,44 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; -import java.util.*; -import java.util.concurrent.*; -import java.util.stream.Collector; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toList; public class MealsUtil { - private static final int DEFAULT_CALORIES_PER_DAY = 2000; + public static final int DEFAULT_CALORIES_PER_DAY = 2000; - public static void main(String[] args) throws ExecutionException, InterruptedException { - List meals = Arrays.asList( - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 1000), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) - ); - LocalTime startTime = LocalTime.of(7, 0); - LocalTime endTime = LocalTime.of(12, 0); + public static final List MEALS = Arrays.asList( + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) + ); - List mealsTo = getFiltered(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY); - mealsTo.forEach(System.out::println); + public static List getTos(List meals, int caloriesPerDay) { + return getFiltered(meals, caloriesPerDay, meal -> true); + } - System.out.println(getFilteredByCycle(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); - System.out.println(getFilteredByRecursion(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); -// System.out.println(getFilteredByAtomic(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); -// System.out.println(getFilteredByClosure(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); - System.out.println(getFilteredByExecutor(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); - System.out.println(getFilteredByFlatMap(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); - System.out.println(getFilteredByCollector(meals, startTime, endTime, DEFAULT_CALORIES_PER_DAY)); + public static List getFilteredTos(List meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { + return getFiltered(meals, caloriesPerDay, meal -> DateTimeUtil.isBetween(meal.getTime(), startTime, endTime)); } - public static List getFiltered(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + private static List getFiltered(List 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(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime)) + .filter(filter) .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) .collect(Collectors.toList()); } - private static List getFilteredByCycle(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - final Map caloriesSumByDate = new HashMap<>(); - meals.forEach(meal -> caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum)); - - final List mealsTo = new ArrayList<>(); - meals.forEach(meal -> { - if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { - mealsTo.add(createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)); - } - }); - return mealsTo; - } - - private static List getFilteredByRecursion(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - ArrayList result = new ArrayList<>(); - filterWithRecursion(new LinkedList<>(meals), startTime, endTime, caloriesPerDay, new HashMap<>(), result); - return result; - } - - private static void filterWithRecursion(LinkedList meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay, - Map dailyCaloriesMap, List result) { - if (meals.isEmpty()) return; - - Meal meal = meals.pop(); - dailyCaloriesMap.merge(meal.getDate(), meal.getCalories(), Integer::sum); - filterWithRecursion(meals, startTime, endTime, caloriesPerDay, dailyCaloriesMap, result); - if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { - result.add(createTo(meal, dailyCaloriesMap.get(meal.getDate()) > caloriesPerDay)); - } - } - -/* - private static List getFilteredByAtomic(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - Map caloriesSumByDate = new HashMap<>(); - Map exceededMap = new HashMap<>(); - - List mealsTo = new ArrayList<>(); - meals.forEach(meal -> { - AtomicBoolean wrapBoolean = exceededMap.computeIfAbsent(meal.getDate(), date -> new AtomicBoolean()); - Integer dailyCalories = caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum); - if (dailyCalories > caloriesPerDay) { - wrapBoolean.set(true); - } - if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { - mealsTo.add(createTo(meal, wrapBoolean)); // also change createWithExcess and MealTo.excess - } - }); - return mealsTo; - } - - private static List getFilteredByClosure(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - final Map caloriesSumByDate = new HashMap<>(); - List mealsTo = new ArrayList<>(); - mealList.forEach(meal -> { - caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum); - if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { - mealsTo.add(createTo(meal, () -> (caloriesSumByDate.get(meal.getDate()) > caloriesPerDay))); // also change createWithExcess and MealTo.excess - } - } - ); - return mealsTo; - } -*/ - - private static List getFilteredByExecutor(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) throws InterruptedException, ExecutionException { - Map caloriesSumByDate = new HashMap<>(); - List> tasks = new ArrayList<>(); - - meals.forEach(meal -> { - caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum); - if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { - tasks.add(() -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)); - } - }); - ExecutorService executorService = Executors.newFixedThreadPool(4); - List> futures = executorService.invokeAll(tasks); - executorService.shutdown(); - final List mealsTo = new ArrayList<>(); - for (Future future : futures) { - mealsTo.add(future.get()); - } - return mealsTo; - } - - private static List getFilteredByFlatMap(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - Collection> list = meals.stream() - .collect(Collectors.groupingBy(Meal::getDate)).values(); - - return list.stream() - .flatMap(dayMeals -> { - boolean excess = dayMeals.stream().mapToInt(Meal::getCalories).sum() > caloriesPerDay; - return dayMeals.stream().filter(meal -> - TimeUtil.isBetween(meal.getTime(), startTime, endTime)) - .map(meal -> createTo(meal, excess)); - }).collect(toList()); - } - - private static List getFilteredByCollector(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - final class Aggregate { - private final List dailyMeals = new ArrayList<>(); - private int dailySumOfCalories; - - private void accumulate(Meal meal) { - dailySumOfCalories += meal.getCalories(); - if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { - dailyMeals.add(meal); - } - } - - // never invoked if the upstream is sequential - private Aggregate combine(Aggregate that) { - this.dailySumOfCalories += that.dailySumOfCalories; - this.dailyMeals.addAll(that.dailyMeals); - return this; - } - - private Stream finisher() { - final boolean excess = dailySumOfCalories > caloriesPerDay; - return dailyMeals.stream().map(meal -> createTo(meal, excess)); - } - } - - Collection> values = meals.stream() - .collect(Collectors.groupingBy(Meal::getDate, - Collector.of(Aggregate::new, Aggregate::accumulate, Aggregate::combine, Aggregate::finisher)) - ).values(); - - return values.stream().flatMap(identity()).collect(toList()); - } - private static MealTo createTo(Meal meal, boolean excess) { return new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); } diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java deleted file mode 100644 index b7eb2af6f93e..000000000000 --- a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.javawebinar.topjava.util; - -import java.time.LocalTime; - -public class TimeUtil { - public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { - return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java new file mode 100644 index 000000000000..3c3a85c01bb0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -0,0 +1,22 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.util.MealsUtil; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MealServlet extends HttpServlet { + private static final Logger log = LoggerFactory.getLogger(MealServlet.class); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + log.info("getAll"); + request.setAttribute("meals", MealsUtil.getTos(MealsUtil.MEALS, MealsUtil.DEFAULT_CALORIES_PER_DAY)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index ef52d67576c0..f6cf12e69976 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -15,9 +15,7 @@ public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.debug("redirect to users"); - -// request.getRequestDispatcher("/users.jsp").forward(request, response); - response.sendRedirect("users.jsp"); + log.debug("forward to users"); + request.getRequestDispatcher("/users.jsp").forward(request, response); } } 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 index c63810c43593..bd98d3bf3f6a 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -16,4 +16,14 @@ /users + + mealServlet + ru.javawebinar.topjava.web.MealServlet + 0 + + + mealServlet + /meals + + diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 6253517f8b84..cd88b335a454 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -8,6 +8,7 @@

Проект Users +
  • Meals
  • diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp new file mode 100644 index 000000000000..3ed8945ffc77 --- /dev/null +++ b/src/main/webapp/meals.jsp @@ -0,0 +1,48 @@ +<%@ 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="fn" uri="http://topjava.javawebinar.ru/functions" %> +<%--<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>--%> + + + Meal list + + + +
    +

    Home

    +
    +

    Meals

    + + + + + + + + + + + + + + + + +
    DateDescriptionCalories
    + <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> + <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> + <%--${fn:replace(meal.dateTime, 'T', ' ')}--%> + ${fn:formatDateTime(meal.dateTime)} + ${meal.description}${meal.calories}
    +
    + + \ No newline at end of file From f731fc0632b340ab8d0e9c3ae4fe3395512f3b4e Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:34:19 +0300 Subject: [PATCH 27/97] Applied patch - 2_2_HW1_optional --- .../ru/javawebinar/topjava/model/Meal.java | 29 +++++++++ .../ru/javawebinar/topjava/model/MealTo.java | 12 +++- .../repository/InMemoryMealRepository.java | 45 +++++++++++++ .../topjava/repository/MealRepository.java | 18 ++++++ .../javawebinar/topjava/util/MealsUtil.java | 9 +-- .../javawebinar/topjava/web/MealServlet.java | 63 ++++++++++++++++++- src/main/webapp/mealForm.jsp | 51 +++++++++++++++ src/main/webapp/meals.jsp | 6 ++ 8 files changed, 224 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/MealRepository.java create mode 100644 src/main/webapp/mealForm.jsp diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 943ff5cd59fa..3abbee42511e 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -5,6 +5,8 @@ import java.time.LocalTime; public class Meal { + private Integer id; + private final LocalDateTime dateTime; private final String description; @@ -12,11 +14,24 @@ public class Meal { private final int calories; public Meal(LocalDateTime dateTime, String description, int calories) { + this(null, dateTime, description, calories); + } + + public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { + this.id = id; this.dateTime = dateTime; this.description = description; this.calories = calories; } + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + public LocalDateTime getDateTime() { return dateTime; } @@ -36,4 +51,18 @@ public LocalDate getDate() { public LocalTime getTime() { return dateTime.toLocalTime(); } + + public boolean isNew() { + return id == null; + } + + @Override + public String toString() { + return "Meal{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + '}'; + } } diff --git a/src/main/java/ru/javawebinar/topjava/model/MealTo.java b/src/main/java/ru/javawebinar/topjava/model/MealTo.java index 697ad5ef99bf..3e502d41f3af 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealTo.java @@ -3,6 +3,8 @@ import java.time.LocalDateTime; public class MealTo { + private final Integer id; + private final LocalDateTime dateTime; private final String description; @@ -11,13 +13,18 @@ public class MealTo { private final boolean excess; - public MealTo(LocalDateTime dateTime, String description, int calories, boolean excess) { + public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) { + this.id = id; this.dateTime = dateTime; this.description = description; this.calories = calories; this.excess = excess; } + public Integer getId() { + return id; + } + public LocalDateTime getDateTime() { return dateTime; } @@ -37,7 +44,8 @@ public boolean isExcess() { @Override public String toString() { return "MealTo{" + - "dateTime=" + dateTime + + "id=" + id + + ", dateTime=" + dateTime + ", description='" + description + '\'' + ", calories=" + calories + ", excess=" + excess + diff --git a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java new file mode 100644 index 000000000000..01ab32a1eaea --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.util.MealsUtil; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class InMemoryMealRepository implements MealRepository { + private Map repository = new ConcurrentHashMap<>(); + private AtomicInteger counter = new AtomicInteger(0); + + { + MealsUtil.MEALS.forEach(this::save); + } + + @Override + public Meal save(Meal meal) { + if (meal.isNew()) { + meal.setId(counter.incrementAndGet()); + repository.put(meal.getId(), meal); + return meal; + } + // treat case: update, but not present in storage + return repository.computeIfPresent(meal.getId(), (id, oldMeal) -> meal); + } + + @Override + public boolean delete(int id) { + return repository.remove(id) != null; + } + + @Override + public Meal get(int id) { + return repository.get(id); + } + + @Override + public Collection getAll() { + return repository.values(); + } +} + 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..2cb2aef8ba8d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -0,0 +1,18 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; + +import java.util.Collection; + +public interface MealRepository { + // null if not found, when updated + Meal save(Meal meal); + + // false if not found + boolean delete(int id); + + // null if not found + Meal get(int id); + + Collection getAll(); +} diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index a7f92b9d1796..01684b5b90f3 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -8,6 +8,7 @@ import java.time.LocalTime; import java.time.Month; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -25,15 +26,15 @@ public class MealsUtil { new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) ); - public static List getTos(List meals, int caloriesPerDay) { + public static List getTos(Collection meals, int caloriesPerDay) { return getFiltered(meals, caloriesPerDay, meal -> true); } - public static List getFilteredTos(List meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { + public static List getFilteredTos(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { return getFiltered(meals, caloriesPerDay, meal -> DateTimeUtil.isBetween(meal.getTime(), startTime, endTime)); } - private static List getFiltered(List meals, int caloriesPerDay, Predicate filter) { + private static List getFiltered(Collection meals, int caloriesPerDay, Predicate filter) { Map caloriesSumByDate = meals.stream() .collect( Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) @@ -46,6 +47,6 @@ private static List getFiltered(List meals, int caloriesPerDay, Pr } private static MealTo createTo(Meal meal, boolean excess) { - return new MealTo(meal.getDateTime(), meal.getDescription(), meal.getCalories(), 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/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 3c3a85c01bb0..0f5597456a99 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -2,21 +2,78 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.InMemoryMealRepository; +import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; +import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; public class MealServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(MealServlet.class); + private MealRepository repository; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + repository = new InMemoryMealRepository(); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + request.setCharacterEncoding("UTF-8"); + String id = request.getParameter("id"); + + Meal meal = new Meal(id.isEmpty() ? null : Integer.valueOf(id), + LocalDateTime.parse(request.getParameter("dateTime")), + request.getParameter("description"), + Integer.parseInt(request.getParameter("calories"))); + + log.info(meal.isNew() ? "Create {}" : "Update {}", meal); + repository.save(meal); + response.sendRedirect("meals"); + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.info("getAll"); - request.setAttribute("meals", MealsUtil.getTos(MealsUtil.MEALS, MealsUtil.DEFAULT_CALORIES_PER_DAY)); - request.getRequestDispatcher("/meals.jsp").forward(request, response); + String action = request.getParameter("action"); + + switch (action == null ? "all" : action) { + case "delete": + int id = getId(request); + log.info("Delete {}", id); + repository.delete(id); + response.sendRedirect("meals"); + break; + case "create": + case "update": + final Meal meal = "create".equals(action) ? + new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : + repository.get(getId(request)); + request.setAttribute("meal", meal); + request.getRequestDispatcher("/mealForm.jsp").forward(request, response); + break; + case "all": + default: + log.info("getAll"); + request.setAttribute("meals", + MealsUtil.getTos(repository.getAll(), MealsUtil.DEFAULT_CALORIES_PER_DAY)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + break; + } + } + + private int getId(HttpServletRequest request) { + String paramId = Objects.requireNonNull(request.getParameter("id")); + return Integer.parseInt(paramId); } } diff --git a/src/main/webapp/mealForm.jsp b/src/main/webapp/mealForm.jsp new file mode 100644 index 000000000000..018300082f0e --- /dev/null +++ b/src/main/webapp/mealForm.jsp @@ -0,0 +1,51 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + Meal + + + +
    +

    Home

    +
    +

    ${param.action == 'create' ? 'Create meal' : 'Edit meal'}

    + +
    + +
    +
    DateTime:
    +
    +
    +
    +
    Description:
    +
    +
    +
    +
    Calories:
    +
    +
    + + +
    +
    + + diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index 3ed8945ffc77..b90cadc9f2f2 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -21,12 +21,16 @@

    Home


    Meals

    + Add Meal +

    + + @@ -40,6 +44,8 @@ + +
    Date Description Calories
    ${meal.description} ${meal.calories}UpdateDelete
    From 95d650086318b0f1889245a8e7d644afaad63ee6 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:35:08 +0300 Subject: [PATCH 28/97] Applied patch - 2_3_app_layers --- .../topjava/model/AbstractBaseEntity.java | 26 ++++++ .../topjava/model/AbstractNamedEntity.java | 24 +++++ .../ru/javawebinar/topjava/model/Role.java | 6 ++ .../ru/javawebinar/topjava/model/User.java | 91 +++++++++++++++++++ .../topjava/repository/UserRepository.java | 21 +++++ .../topjava/service/MealService.java | 9 ++ .../topjava/service/UserService.java | 39 ++++++++ .../topjava/util/ValidationUtil.java | 42 +++++++++ .../util/exception/NotFoundException.java | 7 ++ .../javawebinar/topjava/web/SecurityUtil.java | 14 +++ .../topjava/web/meal/MealRestController.java | 8 ++ .../web/user/AbstractUserController.java | 49 ++++++++++ .../topjava/web/user/AdminRestController.java | 38 ++++++++ .../web/user/ProfileRestController.java | 20 ++++ 14 files changed, 394 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/Role.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/User.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/UserRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/service/MealService.java create mode 100644 src/main/java/ru/javawebinar/topjava/service/UserService.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java 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..8f27c902e047 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -0,0 +1,26 @@ +package ru.javawebinar.topjava.model; + +public abstract class AbstractBaseEntity { + protected Integer id; + + protected AbstractBaseEntity(Integer id) { + this.id = id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public boolean isNew() { + return this.id == null; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ":" + 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..2054a3d3cf9b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.model; + +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + protected String name; + + protected AbstractNamedEntity(Integer id, String name) { + super(id); + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return super.toString() + '(' + name + ')'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java new file mode 100644 index 000000000000..84d62071ad9c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -0,0 +1,6 @@ +package ru.javawebinar.topjava.model; + +public enum Role { + ROLE_USER, + ROLE_ADMIN +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java new file mode 100644 index 000000000000..d88e381945d8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -0,0 +1,91 @@ +package ru.javawebinar.topjava.model; + +import java.util.Date; +import java.util.EnumSet; +import java.util.Set; + +import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; + +public class User extends AbstractNamedEntity { + + private String email; + + private String password; + + private boolean enabled = true; + + private Date registered = new Date(); + + private Set roles; + + private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + + public User(Integer id, String name, String email, String password, Role role, Role... roles) { + this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, EnumSet.of(role, roles)); + } + + public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Set roles) { + super(id, name); + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + this.enabled = enabled; + this.roles = roles; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getRegistered() { + return registered; + } + + public void setRegistered(Date registered) { + this.registered = registered; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getCaloriesPerDay() { + return caloriesPerDay; + } + + public void setCaloriesPerDay(int caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public boolean isEnabled() { + return enabled; + } + + public Set getRoles() { + return roles; + } + + public String getPassword() { + return password; + } + + @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/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java new file mode 100644 index 000000000000..138369789175 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -0,0 +1,21 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public interface UserRepository { + // null if not found, when updated + User save(User user); + + // false if not found + boolean delete(int id); + + // null if not found + User get(int id); + + // null if not found + User getByEmail(String email); + + List getAll(); +} \ No newline at end of file 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..0dc4a43c394f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -0,0 +1,9 @@ +package ru.javawebinar.topjava.service; + +import ru.javawebinar.topjava.repository.MealRepository; + +public class MealService { + + private MealRepository repository; + +} \ 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..c13e8d3d0f65 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava.service; + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +public class UserService { + + private UserRepository repository; + + public User create(User user) { + return repository.save(user); + } + + public void delete(int id) throws NotFoundException { + checkNotFoundWithId(repository.delete(id), id); + } + + public User get(int id) throws NotFoundException { + return checkNotFoundWithId(repository.get(id), id); + } + + public User getByEmail(String email) throws NotFoundException { + return checkNotFound(repository.getByEmail(email), "email=" + email); + } + + public List getAll() { + return repository.getAll(); + } + + public void update(User user) throws NotFoundException { + checkNotFoundWithId(repository.save(user), user.getId()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java new file mode 100644 index 000000000000..f38a2d255603 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava.util; + + +import ru.javawebinar.topjava.model.AbstractBaseEntity; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +public class 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 msg) { + if (!found) { + throw new NotFoundException("Not found entity with " + msg); + } + } + + public static void checkNew(AbstractBaseEntity entity) { + if (!entity.isNew()) { + throw new IllegalArgumentException(entity + " must be new (id=null)"); + } + } + + public static void assureIdConsistent(AbstractBaseEntity entity, int id) { +// conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473) + if (entity.isNew()) { + entity.setId(id); + } else if (entity.getId() != id) { + throw new IllegalArgumentException(entity + " must be with id=" + id); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java new file mode 100644 index 000000000000..f1e9b0e46376 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java new file mode 100644 index 000000000000..e78a4b284a9a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -0,0 +1,14 @@ +package ru.javawebinar.topjava.web; + +import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; + +public class SecurityUtil { + + public static int authUserId() { + return 1; + } + + public static int authUserCaloriesPerDay() { + return DEFAULT_CALORIES_PER_DAY; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java new file mode 100644 index 000000000000..ab4e8ea8bb8e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -0,0 +1,8 @@ +package ru.javawebinar.topjava.web.meal; + +import ru.javawebinar.topjava.service.MealService; + +public class MealRestController { + private MealService service; + +} \ 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..16a68259ec20 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -0,0 +1,49 @@ +package ru.javawebinar.topjava.web.user; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +public abstract class AbstractUserController { + protected final Logger log = LoggerFactory.getLogger(getClass()); + + private UserService service; + + public List getAll() { + log.info("getAll"); + return service.getAll(); + } + + public User get(int id) { + log.info("get {}", id); + return service.get(id); + } + + public User create(User user) { + log.info("create {}", user); + checkNew(user); + return service.create(user); + } + + public void delete(int id) { + log.info("delete {}", id); + service.delete(id); + } + + public void update(User user, int id) { + log.info("update {} with id={}", user, id); + assureIdConsistent(user, id); + service.update(user); + } + + public User getByMail(String email) { + log.info("getByEmail {}", email); + return service.getByEmail(email); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java new file mode 100644 index 000000000000..ae3374468e41 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.web.user; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public class AdminRestController extends AbstractUserController { + + @Override + public List getAll() { + return super.getAll(); + } + + @Override + public User get(int id) { + return super.get(id); + } + + @Override + public User create(User user) { + return super.create(user); + } + + @Override + public void delete(int id) { + super.delete(id); + } + + @Override + public void update(User user, int id) { + super.update(user, id); + } + + @Override + public User getByMail(String email) { + return super.getByMail(email); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java new file mode 100644 index 000000000000..b5062e20bc90 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.web.user; + +import ru.javawebinar.topjava.model.User; + +import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; + +public class ProfileRestController extends AbstractUserController { + + public User get() { + return super.get(authUserId()); + } + + public void delete() { + super.delete(authUserId()); + } + + public void update(User user) { + super.update(user, authUserId()); + } +} \ No newline at end of file From c3009561d3cbee7ed26628c00c39061647947ba8 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:35:43 +0300 Subject: [PATCH 29/97] Applied patch - 2_4_add_spring_context --- pom.xml | 16 ++++--- .../ru/javawebinar/topjava/SpringMain.java | 19 ++++++++ .../InMemoryMealRepository.java | 3 +- .../inmemory/InMemoryUserRepository.java | 43 +++++++++++++++++++ .../javawebinar/topjava/web/MealServlet.java | 2 +- src/main/resources/spring/spring-app.xml | 7 +++ 6 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/SpringMain.java rename src/main/java/ru/javawebinar/topjava/repository/{ => inmemory}/InMemoryMealRepository.java (91%) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java create mode 100644 src/main/resources/spring/spring-app.xml diff --git a/pom.xml b/pom.xml index 62fe2c95d1d8..7d72bec29461 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,8 @@ UTF-8 UTF-8 + 5.2.0.RELEASE + 1.2.3 1.7.28 @@ -46,13 +48,6 @@ compile - - org.slf4j - jcl-over-slf4j - ${slf4j.version} - runtime - - ch.qos.logback logback-classic @@ -60,6 +55,13 @@ runtime + + + org.springframework + spring-context + ${spring.version} + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java new file mode 100644 index 000000000000..e836531c305a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Arrays; + +public class SpringMain { + public static void main(String[] args) { + ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); + +// UserRepository userRepository = (UserRepository) appCtx.getBean("inmemoryUserRepository"); + UserRepository userRepository = appCtx.getBean(UserRepository.class); + userRepository.getAll(); + appCtx.close(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java similarity index 91% rename from src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java rename to src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 01ab32a1eaea..d1e500dab254 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -1,6 +1,7 @@ -package ru.javawebinar.topjava.repository; +package ru.javawebinar.topjava.repository.inmemory; import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; import java.util.Collection; diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java new file mode 100644 index 000000000000..c9bcb8a4f289 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -0,0 +1,43 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Collections; +import java.util.List; + +public class InMemoryUserRepository implements UserRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryUserRepository.class); + + @Override + public boolean delete(int id) { + log.info("delete {}", id); + return true; + } + + @Override + public User save(User user) { + log.info("save {}", user); + return user; + } + + @Override + public User get(int id) { + log.info("get {}", id); + return null; + } + + @Override + public List getAll() { + log.info("getAll"); + return Collections.emptyList(); + } + + @Override + public User getByEmail(String email) { + log.info("getByEmail {}", email); + return null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 0f5597456a99..15dd92dc9741 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -3,8 +3,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.repository.InMemoryMealRepository; import ru.javawebinar.topjava.repository.MealRepository; +import ru.javawebinar.topjava.repository.inmemory.InMemoryMealRepository; import ru.javawebinar.topjava.util.MealsUtil; import javax.servlet.ServletConfig; diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml new file mode 100644 index 000000000000..3a32c66999dc --- /dev/null +++ b/src/main/resources/spring/spring-app.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file From 29d1db095575716ee63f83d016673009058bd89d Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:36:04 +0300 Subject: [PATCH 30/97] Applied patch - 2_5_dependency_injection --- src/main/java/ru/javawebinar/topjava/SpringMain.java | 7 +++++++ .../java/ru/javawebinar/topjava/service/UserService.java | 4 ++++ src/main/resources/spring/spring-app.xml | 3 +++ 3 files changed, 14 insertions(+) diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java index e836531c305a..44217377d8da 100644 --- a/src/main/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -2,7 +2,10 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.service.UserService; import java.util.Arrays; @@ -14,6 +17,10 @@ public static void main(String[] args) { // UserRepository userRepository = (UserRepository) appCtx.getBean("inmemoryUserRepository"); UserRepository userRepository = appCtx.getBean(UserRepository.class); userRepository.getAll(); + + UserService userService = appCtx.getBean(UserService.class); + userService.create(new User(null, "userName", "email@mail.ru", "password", Role.ROLE_ADMIN)); + appCtx.close(); } } diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index c13e8d3d0f65..6303c4df18b4 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -13,6 +13,10 @@ public class UserService { private UserRepository repository; + public void setRepository(UserRepository repository) { + this.repository = repository; + } + public User create(User user) { return repository.save(user); } diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 3a32c66999dc..d860094d37f5 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -4,4 +4,7 @@ + + + \ No newline at end of file From ff6a1cd4edaf0bcbad870cf72ac21f38f9ee8860 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:36:28 +0300 Subject: [PATCH 31/97] Applied patch - 2_6_annotation_processing --- .../ru/javawebinar/topjava/SpringMain.java | 20 ++++++---------- .../inmemory/InMemoryUserRepository.java | 2 ++ .../topjava/service/UserService.java | 8 +++---- .../web/user/AbstractUserController.java | 2 ++ .../topjava/web/user/AdminRestController.java | 2 ++ .../web/user/ProfileRestController.java | 2 ++ src/main/resources/spring/spring-app.xml | 24 +++++++++++++++---- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java index 44217377d8da..f4ba6a775278 100644 --- a/src/main/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -4,23 +4,17 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; -import ru.javawebinar.topjava.repository.UserRepository; -import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.web.user.AdminRestController; import java.util.Arrays; public class SpringMain { public static void main(String[] args) { - ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); - System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); - -// UserRepository userRepository = (UserRepository) appCtx.getBean("inmemoryUserRepository"); - UserRepository userRepository = appCtx.getBean(UserRepository.class); - userRepository.getAll(); - - UserService userService = appCtx.getBean(UserService.class); - userService.create(new User(null, "userName", "email@mail.ru", "password", Role.ROLE_ADMIN)); - - appCtx.close(); + // java 7 automatic resource management + try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml")) { + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); + AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); + adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ROLE_ADMIN)); + } } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index c9bcb8a4f289..e2f8a9b8b10d 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -2,12 +2,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import java.util.Collections; import java.util.List; +@Repository public class InMemoryUserRepository implements UserRepository { private static final Logger log = LoggerFactory.getLogger(InMemoryUserRepository.class); diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 6303c4df18b4..9e761c9bb73e 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -9,14 +11,12 @@ import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; +@Service public class UserService { + @Autowired private UserRepository repository; - public void setRepository(UserRepository repository) { - this.repository = repository; - } - public User create(User user) { return repository.save(user); } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index 16a68259ec20..0000f1c1e02f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; @@ -13,6 +14,7 @@ public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); + @Autowired private UserService service; public List getAll() { diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index ae3374468e41..b37a8ed6c8a5 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -1,9 +1,11 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.stereotype.Controller; import ru.javawebinar.topjava.model.User; import java.util.List; +@Controller public class AdminRestController extends AbstractUserController { @Override diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index b5062e20bc90..7d3702c31c46 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -1,9 +1,11 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.stereotype.Controller; import ru.javawebinar.topjava.model.User; import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; +@Controller public class ProfileRestController extends AbstractUserController { public User get() { diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index d860094d37f5..f15313620cb9 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -1,10 +1,24 @@ + xmlns:context="http://www.springframework.org/schema/context" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> - + + + + + + + + + + - - - \ No newline at end of file From 3c224270932dc2c792f54e56948b25c148b75024 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 11 Oct 2019 00:36:51 +0300 Subject: [PATCH 32/97] Applied patch - 2_7_constructor_injection --- .../java/ru/javawebinar/topjava/service/UserService.java | 6 +++++- src/main/resources/spring/spring-app.xml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 9e761c9bb73e..5a7527bddabe 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -14,8 +14,12 @@ @Service public class UserService { + private final UserRepository repository; + @Autowired - private UserRepository repository; + public UserService(UserRepository repository) { + this.repository = repository; + } public User create(User user) { return repository.save(user); diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index f15313620cb9..31a70253bf5a 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -8,7 +8,7 @@ - + --> From 6340d979f26033cdd862c932eda48d510314d957 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 13:04:56 +0300 Subject: [PATCH 33/97] Applied patch - 3_01_HW2_repository --- .../topjava/repository/MealRepository.java | 21 ++++--- .../inmemory/InMemoryMealRepository.java | 60 ++++++++++++++----- .../inmemory/InMemoryUserRepository.java | 46 ++++++++------ .../topjava/util/DateTimeUtil.java | 5 -- .../javawebinar/topjava/util/MealsUtil.java | 2 +- .../ru/javawebinar/topjava/util/Util.java | 7 +++ .../javawebinar/topjava/web/MealServlet.java | 8 +-- 7 files changed, 100 insertions(+), 49 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/util/Util.java diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java index 2cb2aef8ba8d..f0578ff40457 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -2,17 +2,22 @@ import ru.javawebinar.topjava.model.Meal; -import java.util.Collection; +import java.time.LocalDateTime; +import java.util.List; public interface MealRepository { - // null if not found, when updated - Meal save(Meal meal); + // null if updated meal do not belong to userId + Meal save(Meal meal, int userId); - // false if not found - boolean delete(int id); + // false if meal do not belong to userId + boolean delete(int id, int userId); - // null if not found - Meal get(int id); + // null if meal do not belong to userId + Meal get(int id, int userId); - Collection getAll(); + // ORDERED dateTime desc + List getAll(int userId); + + // ORDERED dateTime desc + List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index d1e500dab254..ff37bdf88360 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -1,46 +1,78 @@ package ru.javawebinar.topjava.repository.inmemory; +import org.springframework.util.CollectionUtils; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.util.Util; -import java.util.Collection; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.ADMIN_ID; +import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.USER_ID; public class InMemoryMealRepository implements MealRepository { - private Map repository = new ConcurrentHashMap<>(); + + // Map userId -> (mealId-> meal) + private Map> usersMealsMap = new ConcurrentHashMap<>(); private AtomicInteger counter = new AtomicInteger(0); { - MealsUtil.MEALS.forEach(this::save); + MealsUtil.MEALS.forEach(meal -> save(meal, USER_ID)); + + save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510), ADMIN_ID); + save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500), ADMIN_ID); } + @Override - public Meal save(Meal meal) { + public Meal save(Meal meal, int userId) { + Map meals = usersMealsMap.computeIfAbsent(userId, ConcurrentHashMap::new); if (meal.isNew()) { meal.setId(counter.incrementAndGet()); - repository.put(meal.getId(), meal); + meals.put(meal.getId(), meal); return meal; } - // treat case: update, but not present in storage - return repository.computeIfPresent(meal.getId(), (id, oldMeal) -> meal); + return meals.computeIfPresent(meal.getId(), (id, oldMeal) -> meal); } @Override - public boolean delete(int id) { - return repository.remove(id) != null; + public boolean delete(int id, int userId) { + Map meals = usersMealsMap.get(userId); + return meals != null && meals.remove(id) != null; } @Override - public Meal get(int id) { - return repository.get(id); + public Meal get(int id, int userId) { + Map meals = usersMealsMap.get(userId); + return meals == null ? null : meals.get(id); } @Override - public Collection getAll() { - return repository.values(); + public List getAll(int userId) { + return getAllFiltered(userId, meal -> true); } -} + @Override + public List getBetween(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return getAllFiltered(userId, meal -> Util.isBetweenInclusive(meal.getDateTime(), startDateTime, endDateTime)); + } + + private List getAllFiltered(int userId, Predicate filter) { + Map meals = usersMealsMap.get(userId); + return CollectionUtils.isEmpty(meals) ? Collections.emptyList() : + meals.values().stream() + .filter(filter) + .sorted(Comparator.comparing(Meal::getDateTime).reversed()) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index e2f8a9b8b10d..4016ec9442af 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -1,45 +1,57 @@ package ru.javawebinar.topjava.repository.inmemory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; -import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; @Repository public class InMemoryUserRepository implements UserRepository { - private static final Logger log = LoggerFactory.getLogger(InMemoryUserRepository.class); + + public static final int USER_ID = 1; + public static final int ADMIN_ID = 2; + + private Map usersMap = new ConcurrentHashMap<>(); + private AtomicInteger counter = new AtomicInteger(0); @Override - public boolean delete(int id) { - log.info("delete {}", id); - return true; + public User save(User user) { + if (user.isNew()) { + user.setId(counter.incrementAndGet()); + usersMap.put(user.getId(), user); + return user; + } + return usersMap.computeIfPresent(user.getId(), (id, oldUser) -> user); } @Override - public User save(User user) { - log.info("save {}", user); - return user; + public boolean delete(int id) { + return usersMap.remove(id) != null; } @Override public User get(int id) { - log.info("get {}", id); - return null; + return usersMap.get(id); } @Override public List getAll() { - log.info("getAll"); - return Collections.emptyList(); + return usersMap.values().stream() + .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) + .collect(Collectors.toList()); } @Override public User getByEmail(String email) { - log.info("getByEmail {}", email); - return null; + return usersMap.values().stream() + .filter(u -> email.equals(u.getEmail())) + .findFirst() + .orElse(null); } -} +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index fc9e659fdca4..e68cc74f1b33 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -1,16 +1,11 @@ package ru.javawebinar.topjava.util; import java.time.LocalDateTime; -import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class DateTimeUtil { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { - return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; - } - public static String toString(LocalDateTime ldt) { return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); } diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 01684b5b90f3..6a5494b94608 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -31,7 +31,7 @@ public static List getTos(Collection meals, int caloriesPerDay) { } public static List getFilteredTos(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { - return getFiltered(meals, caloriesPerDay, meal -> DateTimeUtil.isBetween(meal.getTime(), startTime, endTime)); + return getFiltered(meals, caloriesPerDay, meal -> Util.isBetweenInclusive(meal.getTime(), startTime, endTime)); } private static List getFiltered(Collection meals, int caloriesPerDay, Predicate filter) { 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..dd0f6fdf3e41 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/Util.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util; + +public class Util { + public static > boolean isBetweenInclusive(T value, T start, T end) { + return value.compareTo(start) >= 0 && value.compareTo(end) <= 0; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 15dd92dc9741..6468344a5d81 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -39,7 +39,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) Integer.parseInt(request.getParameter("calories"))); log.info(meal.isNew() ? "Create {}" : "Update {}", meal); - repository.save(meal); + repository.save(meal, SecurityUtil.authUserId()); response.sendRedirect("meals"); } @@ -51,14 +51,14 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t case "delete": int id = getId(request); log.info("Delete {}", id); - repository.delete(id); + repository.delete(id, SecurityUtil.authUserId()); response.sendRedirect("meals"); break; case "create": case "update": final Meal meal = "create".equals(action) ? new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : - repository.get(getId(request)); + repository.get(getId(request), SecurityUtil.authUserId()); request.setAttribute("meal", meal); request.getRequestDispatcher("/mealForm.jsp").forward(request, response); break; @@ -66,7 +66,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t default: log.info("getAll"); request.setAttribute("meals", - MealsUtil.getTos(repository.getAll(), MealsUtil.DEFAULT_CALORIES_PER_DAY)); + MealsUtil.getTos(repository.getAll(SecurityUtil.authUserId()), MealsUtil.DEFAULT_CALORIES_PER_DAY)); request.getRequestDispatcher("/meals.jsp").forward(request, response); break; } From 820934dbbc94c1ca115fec20a99c6e9d7e253383 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 13:05:53 +0300 Subject: [PATCH 34/97] Applied patch - 3_02_HW2_meal_layers --- .../ru/javawebinar/topjava/SpringMain.java | 14 ++++ .../ru/javawebinar/topjava/model/Meal.java | 18 +---- .../inmemory/InMemoryBaseRepository.java | 38 ++++++++++ .../inmemory/InMemoryMealRepository.java | 30 ++++---- .../inmemory/InMemoryUserRepository.java | 36 ++-------- .../topjava/service/MealService.java | 42 ++++++++++- .../topjava/{model => to}/MealTo.java | 2 +- .../topjava/util/DateTimeUtil.java | 8 +++ .../javawebinar/topjava/util/MealsUtil.java | 5 +- .../ru/javawebinar/topjava/util/Util.java | 6 +- .../topjava/web/meal/MealRestController.java | 72 ++++++++++++++++++- src/main/webapp/meals.jsp | 2 +- 12 files changed, 200 insertions(+), 73 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java rename src/main/java/ru/javawebinar/topjava/{model => to}/MealTo.java (96%) diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java index f4ba6a775278..f72561bbd4b8 100644 --- a/src/main/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -4,9 +4,15 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.web.meal.MealRestController; import ru.javawebinar.topjava.web.user.AdminRestController; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Month; import java.util.Arrays; +import java.util.List; public class SpringMain { public static void main(String[] args) { @@ -15,6 +21,14 @@ public static void main(String[] args) { System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ROLE_ADMIN)); + System.out.println(); + + MealRestController mealController = appCtx.getBean(MealRestController.class); + List filteredMealsWithExcess = + mealController.getBetween( + LocalDate.of(2015, Month.MAY, 30), LocalTime.of(7, 0), + LocalDate.of(2015, Month.MAY, 31), LocalTime.of(11, 0)); + filteredMealsWithExcess.forEach(System.out::println); } } } diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 3abbee42511e..9eed15f706be 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -4,9 +4,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; -public class Meal { - private Integer id; - +public class Meal extends AbstractBaseEntity { private final LocalDateTime dateTime; private final String description; @@ -18,20 +16,12 @@ public Meal(LocalDateTime dateTime, String description, int calories) { } public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { - this.id = id; + super(id); this.dateTime = dateTime; this.description = description; this.calories = calories; } - public Integer getId() { - return id; - } - - public void setId(Integer id) { - this.id = id; - } - public LocalDateTime getDateTime() { return dateTime; } @@ -52,10 +42,6 @@ public LocalTime getTime() { return dateTime.toLocalTime(); } - public boolean isNew() { - return id == null; - } - @Override public String toString() { return "Meal{" + diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java new file mode 100644 index 000000000000..b0ccb85b0674 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -0,0 +1,38 @@ +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.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +@Repository +public class InMemoryBaseRepository { + + private static AtomicInteger counter = new AtomicInteger(0); + + private Map map = new ConcurrentHashMap<>(); + + public T save(T entry) { + if (entry.isNew()) { + entry.setId(counter.incrementAndGet()); + map.put(entry.getId(), entry); + return entry; + } + return map.computeIfPresent(entry.getId(), (id, oldT) -> entry); + } + + public boolean delete(int id) { + return map.remove(id) != null; + } + + public T get(int id) { + return map.get(id); + } + + Collection getCollection() { + return map.values(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index ff37bdf88360..38ee94a9a802 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -1,6 +1,6 @@ package ru.javawebinar.topjava.repository.inmemory; -import org.springframework.util.CollectionUtils; +import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; @@ -13,18 +13,17 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Collectors; import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.ADMIN_ID; import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.USER_ID; +@Repository public class InMemoryMealRepository implements MealRepository { - // Map userId -> (mealId-> meal) - private Map> usersMealsMap = new ConcurrentHashMap<>(); - private AtomicInteger counter = new AtomicInteger(0); + // Map userId -> mealRepository + private Map> usersMealsMap = new ConcurrentHashMap<>(); { MealsUtil.MEALS.forEach(meal -> save(meal, USER_ID)); @@ -36,24 +35,19 @@ public class InMemoryMealRepository implements MealRepository { @Override public Meal save(Meal meal, int userId) { - Map meals = usersMealsMap.computeIfAbsent(userId, ConcurrentHashMap::new); - if (meal.isNew()) { - meal.setId(counter.incrementAndGet()); - meals.put(meal.getId(), meal); - return meal; - } - return meals.computeIfPresent(meal.getId(), (id, oldMeal) -> meal); + InMemoryBaseRepository meals = usersMealsMap.computeIfAbsent(userId, uid -> new InMemoryBaseRepository<>()); + return meals.save(meal); } @Override public boolean delete(int id, int userId) { - Map meals = usersMealsMap.get(userId); - return meals != null && meals.remove(id) != null; + InMemoryBaseRepository meals = usersMealsMap.get(userId); + return meals != null && meals.delete(id); } @Override public Meal get(int id, int userId) { - Map meals = usersMealsMap.get(userId); + InMemoryBaseRepository meals = usersMealsMap.get(userId); return meals == null ? null : meals.get(id); } @@ -68,9 +62,9 @@ public List getBetween(LocalDateTime startDateTime, LocalDateTime endDateT } private List getAllFiltered(int userId, Predicate filter) { - Map meals = usersMealsMap.get(userId); - return CollectionUtils.isEmpty(meals) ? Collections.emptyList() : - meals.values().stream() + InMemoryBaseRepository meals = usersMealsMap.get(userId); + return meals == null ? Collections.emptyList() : + meals.getCollection().stream() .filter(filter) .sorted(Comparator.comparing(Meal::getDateTime).reversed()) .collect(Collectors.toList()); diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index 4016ec9442af..f8968a815041 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -6,50 +6,24 @@ import java.util.Comparator; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @Repository -public class InMemoryUserRepository implements UserRepository { +public class InMemoryUserRepository extends InMemoryBaseRepository implements UserRepository { - public static final int USER_ID = 1; - public static final int ADMIN_ID = 2; - - private Map usersMap = new ConcurrentHashMap<>(); - private AtomicInteger counter = new AtomicInteger(0); - - @Override - public User save(User user) { - if (user.isNew()) { - user.setId(counter.incrementAndGet()); - usersMap.put(user.getId(), user); - return user; - } - return usersMap.computeIfPresent(user.getId(), (id, oldUser) -> user); - } - - @Override - public boolean delete(int id) { - return usersMap.remove(id) != null; - } - - @Override - public User get(int id) { - return usersMap.get(id); - } + static final int USER_ID = 1; + static final int ADMIN_ID = 2; @Override public List getAll() { - return usersMap.values().stream() + return getCollection().stream() .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) .collect(Collectors.toList()); } @Override public User getByEmail(String email) { - return usersMap.values().stream() + return getCollection().stream() .filter(u -> email.equals(u.getEmail())) .findFirst() .orElse(null); diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 0dc4a43c394f..1e433b1d5158 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -1,9 +1,49 @@ package ru.javawebinar.topjava.service; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; +import ru.javawebinar.topjava.util.DateTimeUtil; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service public class MealService { - private MealRepository repository; + private final MealRepository repository; + + public MealService(MealRepository repository) { + this.repository = repository; + } + + public Meal get(int id, int userId) { + return checkNotFoundWithId(repository.get(id, userId), id); + } + + public void delete(int id, int userId) { + checkNotFoundWithId(repository.delete(id, userId), id); + } + + public List getBetweenDates(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { + return repository.getBetween( + DateTimeUtil.createDateTime(startDate, LocalDate.MIN, LocalTime.MIN), + DateTimeUtil.createDateTime(endDate, LocalDate.MAX, LocalTime.MAX), userId); + } + + public List getAll(int userId) { + return repository.getAll(userId); + } + + public void update(Meal meal, int userId) { + checkNotFoundWithId(repository.save(meal, userId), meal.getId()); + } + public Meal create(Meal meal, int userId) { + return repository.save(meal, userId); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java similarity index 96% rename from src/main/java/ru/javawebinar/topjava/model/MealTo.java rename to src/main/java/ru/javawebinar/topjava/to/MealTo.java index 3e502d41f3af..eedce0f2f75c 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -1,4 +1,4 @@ -package ru.javawebinar.topjava.model; +package ru.javawebinar.topjava.to; import java.time.LocalDateTime; diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index e68cc74f1b33..a0323232457d 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -1,6 +1,10 @@ package ru.javawebinar.topjava.util; +import org.springframework.lang.Nullable; + +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; public class DateTimeUtil { @@ -9,5 +13,9 @@ public class DateTimeUtil { public static String toString(LocalDateTime ldt) { return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); } + + public static LocalDateTime createDateTime(@Nullable LocalDate date, LocalDate defaultDate, LocalTime time) { + return LocalDateTime.of(date != null ? date : defaultDate, time); + } } diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 6a5494b94608..5a35ba6ff1a3 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -1,7 +1,8 @@ package ru.javawebinar.topjava.util; +import org.springframework.lang.Nullable; import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.model.MealTo; +import ru.javawebinar.topjava.to.MealTo; import java.time.LocalDate; import java.time.LocalDateTime; @@ -30,7 +31,7 @@ public static List getTos(Collection meals, int caloriesPerDay) { return getFiltered(meals, caloriesPerDay, meal -> true); } - public static List getFilteredTos(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { + public static List getFilteredTos(Collection meals, int caloriesPerDay, @Nullable LocalTime startTime, @Nullable LocalTime endTime) { return getFiltered(meals, caloriesPerDay, meal -> Util.isBetweenInclusive(meal.getTime(), startTime, endTime)); } diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java index dd0f6fdf3e41..bea8af11cb74 100644 --- a/src/main/java/ru/javawebinar/topjava/util/Util.java +++ b/src/main/java/ru/javawebinar/topjava/util/Util.java @@ -1,7 +1,9 @@ package ru.javawebinar.topjava.util; +import org.springframework.lang.Nullable; + public class Util { - public static > boolean isBetweenInclusive(T value, T start, T end) { - return value.compareTo(start) >= 0 && value.compareTo(end) <= 0; + public static > boolean isBetweenInclusive(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/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index ab4e8ea8bb8e..cddd5acee7b7 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -1,8 +1,78 @@ package ru.javawebinar.topjava.web.meal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Controller; +import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.web.SecurityUtil; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +@Controller public class MealRestController { - private MealService service; + private static final Logger log = LoggerFactory.getLogger(MealRestController.class); + + private final MealService service; + + @Autowired + public MealRestController(MealService service) { + this.service = service; + } + + public Meal get(int id) { + int userId = SecurityUtil.authUserId(); + log.info("get meal {} for user {}", id, userId); + return service.get(id, userId); + } + + public void delete(int id) { + int userId = SecurityUtil.authUserId(); + log.info("delete meal {} for user {}", id, userId); + service.delete(id, userId); + } + + public List getAll() { + int userId = SecurityUtil.authUserId(); + log.info("getAll for user {}", userId); + return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay()); + } + + public Meal create(Meal meal) { + int userId = SecurityUtil.authUserId(); + 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(@Nullable LocalDate startDate, @Nullable LocalTime startTime, + @Nullable LocalDate endDate, @Nullable LocalTime endTime) { + int userId = SecurityUtil.authUserId(); + log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); + List mealsDateFiltered = service.getBetweenDates(startDate, endDate, userId); + return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); + } } \ No newline at end of file diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index b90cadc9f2f2..6310069cc863 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -34,7 +34,7 @@ - + <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> From b06ae1683a78078bfd8f3345f34182702dc9cb62 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 13:06:23 +0300 Subject: [PATCH 35/97] Applied patch - 3_03_HW2_optional_MealServlet --- .../javawebinar/topjava/web/MealServlet.java | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 6468344a5d81..cf94cc302ba4 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -1,11 +1,10 @@ package ru.javawebinar.topjava.web; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.util.StringUtils; import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.repository.MealRepository; -import ru.javawebinar.topjava.repository.inmemory.InMemoryMealRepository; -import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.web.meal.MealRestController; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -18,28 +17,36 @@ import java.util.Objects; public class MealServlet extends HttpServlet { - private static final Logger log = LoggerFactory.getLogger(MealServlet.class); - private MealRepository repository; + private ConfigurableApplicationContext springContext; + private MealRestController mealController; @Override public void init(ServletConfig config) throws ServletException { super.init(config); - repository = new InMemoryMealRepository(); + springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + mealController = springContext.getBean(MealRestController.class); + } + + @Override + public void destroy() { + springContext.close(); + super.destroy(); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); - String id = request.getParameter("id"); - - Meal meal = new Meal(id.isEmpty() ? null : Integer.valueOf(id), + Meal meal = new Meal( LocalDateTime.parse(request.getParameter("dateTime")), request.getParameter("description"), Integer.parseInt(request.getParameter("calories"))); - log.info(meal.isNew() ? "Create {}" : "Update {}", meal); - repository.save(meal, SecurityUtil.authUserId()); + if (StringUtils.isEmpty(request.getParameter("id"))) { + mealController.create(meal); + } else { + mealController.update(meal, getId(request)); + } response.sendRedirect("meals"); } @@ -50,23 +57,20 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t switch (action == null ? "all" : action) { case "delete": int id = getId(request); - log.info("Delete {}", id); - repository.delete(id, SecurityUtil.authUserId()); + mealController.delete(id); response.sendRedirect("meals"); break; case "create": case "update": final Meal meal = "create".equals(action) ? new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : - repository.get(getId(request), SecurityUtil.authUserId()); + mealController.get(getId(request)); request.setAttribute("meal", meal); request.getRequestDispatcher("/mealForm.jsp").forward(request, response); break; case "all": default: - log.info("getAll"); - request.setAttribute("meals", - MealsUtil.getTos(repository.getAll(SecurityUtil.authUserId()), MealsUtil.DEFAULT_CALORIES_PER_DAY)); + request.setAttribute("meals", mealController.getAll()); request.getRequestDispatcher("/meals.jsp").forward(request, response); break; } From 4b790a635f0641f752f62e83b87442e6affe42e4 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 13:06:53 +0300 Subject: [PATCH 36/97] Applied patch - 3_04_HW2_optional_filter --- .../topjava/util/DateTimeUtil.java | 9 +++++ .../javawebinar/topjava/web/MealServlet.java | 13 +++++++ src/main/webapp/css/style.css | 24 +++++++++++++ src/main/webapp/mealForm.jsp | 19 +--------- src/main/webapp/meals.jsp | 36 ++++++++++++------- 5 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 src/main/webapp/css/style.css diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index a0323232457d..5f6f3f82e628 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava.util; import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; import java.time.LocalDate; import java.time.LocalDateTime; @@ -14,6 +15,14 @@ public static String toString(LocalDateTime ldt) { return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); } + public static @Nullable LocalDate parseLocalDate(@Nullable String str) { + return StringUtils.isEmpty(str) ? null : LocalDate.parse(str); + } + + public static @Nullable LocalTime parseLocalTime(@Nullable String str) { + return StringUtils.isEmpty(str) ? null : LocalTime.parse(str); + } + public static LocalDateTime createDateTime(@Nullable LocalDate date, LocalDate defaultDate, LocalTime time) { return LocalDateTime.of(date != null ? date : defaultDate, time); } diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index cf94cc302ba4..ec26cd4021a7 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -12,10 +12,15 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.Objects; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; + public class MealServlet extends HttpServlet { private ConfigurableApplicationContext springContext; @@ -68,6 +73,14 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t request.setAttribute("meal", meal); request.getRequestDispatcher("/mealForm.jsp").forward(request, response); break; + case "filter": + LocalDate startDate = parseLocalDate(request.getParameter("startDate")); + LocalDate endDate = parseLocalDate(request.getParameter("endDate")); + LocalTime startTime = parseLocalTime(request.getParameter("startTime")); + LocalTime endTime = parseLocalTime(request.getParameter("endTime")); + request.setAttribute("meals", mealController.getBetween(startDate, startTime, endDate, endTime)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + break; case "all": default: request.setAttribute("meals", mealController.getAll()); diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css new file mode 100644 index 000000000000..26a14ce43e6d --- /dev/null +++ b/src/main/webapp/css/style.css @@ -0,0 +1,24 @@ +dl { + background: none repeat scroll 0 0 #FAFAFA; + margin: 8px 0; + padding: 0; +} + +dt { + display: inline-block; + width: 170px; +} + +dd { + display: inline-block; + margin-left: 8px; + vertical-align: top; +} + +tr[data-mealExcess="false"] { + color: green; +} + +tr[data-mealExcess="true"] { + color: red; +} diff --git a/src/main/webapp/mealForm.jsp b/src/main/webapp/mealForm.jsp index 018300082f0e..e9c01a94c999 100644 --- a/src/main/webapp/mealForm.jsp +++ b/src/main/webapp/mealForm.jsp @@ -4,24 +4,7 @@ Meal - +
    diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index 6310069cc863..16c4b1e16d62 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -2,25 +2,37 @@ <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %> -<%--<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>--%> - Meal list - + Meals +

    Home


    Meals

    +
    + +
    +
    From Date:
    +
    +
    +
    +
    To Date:
    +
    +
    +
    +
    From Time:
    +
    +
    +
    +
    To Time:
    +
    +
    + +
    +
    Add Meal

    @@ -35,7 +47,7 @@ - +
    <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> From d5a447749af5bfd818a7b48dab9af90d9cd21d9f Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 13:07:40 +0300 Subject: [PATCH 37/97] Applied patch - 3_05_HW2_optional_select_user --- .../ru/javawebinar/topjava/web/SecurityUtil.java | 8 +++++++- .../java/ru/javawebinar/topjava/web/UserServlet.java | 7 +++++++ src/main/webapp/index.html | 12 ++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java index e78a4b284a9a..b9639bf1b9ea 100644 --- a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -4,8 +4,14 @@ public class SecurityUtil { + private static int id = 1; + public static int authUserId() { - return 1; + return id; + } + + public static void setAuthUserId(int id) { + SecurityUtil.id = id; } public static int authUserCaloriesPerDay() { diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index f6cf12e69976..226023400c70 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -13,6 +13,13 @@ public class UserServlet extends HttpServlet { private static final Logger log = getLogger(UserServlet.class); + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + int userId = Integer.parseInt(request.getParameter("userId")); + SecurityUtil.setAuthUserId(userId); + response.sendRedirect("meals"); + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { log.debug("forward to users"); diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index cd88b335a454..e68aaa192cd3 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -6,9 +6,13 @@

    Проект Java Enterprise (Topjava)


    - +
    + Meals of  + + +
    From 8127bdb16ecb0a58df2cb94c448c0ff21b8784ea Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 13:11:56 +0300 Subject: [PATCH 38/97] Applied patch - 3_06_bean_life_cycle --- .../inmemory/InMemoryMealRepository.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 38ee94a9a802..29ae505f36c1 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -1,11 +1,15 @@ 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.MealsUtil; import ru.javawebinar.topjava.util.Util; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import java.time.LocalDateTime; import java.time.Month; import java.util.Collections; @@ -21,6 +25,7 @@ @Repository public class InMemoryMealRepository implements MealRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepository.class); // Map userId -> mealRepository private Map> usersMealsMap = new ConcurrentHashMap<>(); @@ -39,6 +44,16 @@ public Meal save(Meal meal, int userId) { 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) { InMemoryBaseRepository meals = usersMealsMap.get(userId); From e8dce1aca75d954fc88cca08ee158e8095d4bddc Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 13:14:36 +0300 Subject: [PATCH 39/97] Applied patch - 3_07_add_junit --- pom.xml | 19 +++++++ .../ru/javawebinar/topjava/SpringMain.java | 0 .../ru/javawebinar/topjava/UserTestData.java | 12 +++++ .../inmemory/InMemoryBaseRepository.java | 2 +- .../inmemory/InMemoryMealRepository.java | 4 +- .../inmemory/InMemoryUserRepository.java | 12 ++++- .../user/InMemoryAdminRestControllerTest.java | 51 +++++++++++++++++++ 7 files changed, 95 insertions(+), 5 deletions(-) rename src/{main => test}/java/ru/javawebinar/topjava/SpringMain.java (100%) create mode 100644 src/test/java/ru/javawebinar/topjava/UserTestData.java rename src/{main => test}/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java (93%) rename src/{main => test}/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java (94%) rename src/{main => test}/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java (73%) create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java diff --git a/pom.xml b/pom.xml index 7d72bec29461..8f3fc31f9d77 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,9 @@ 1.2.3 1.7.28 + + + 4.12 @@ -36,6 +39,14 @@ ${java.version} + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + -Dfile.encoding=UTF-8 + + @@ -75,6 +86,14 @@ jstl 1.2 + + + + junit + junit + ${junit.version} + test + diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java similarity index 100% rename from src/main/java/ru/javawebinar/topjava/SpringMain.java rename to src/test/java/ru/javawebinar/topjava/SpringMain.java 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..196b09362719 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -0,0 +1,12 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; + +public class UserTestData { + public static final int USER_ID = 1; + public static final int ADMIN_ID = 2; + + public static final User USER = new User(USER_ID, "User", "user@yandex.ru", "password", Role.ROLE_USER); + public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ROLE_ADMIN); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java similarity index 93% rename from src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java rename to src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java index b0ccb85b0674..0b6a84c7f2ec 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -13,7 +13,7 @@ public class InMemoryBaseRepository { private static AtomicInteger counter = new AtomicInteger(0); - private Map map = new ConcurrentHashMap<>(); + Map map = new ConcurrentHashMap<>(); public T save(T entry) { if (entry.isNew()) { diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java similarity index 94% rename from src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java rename to src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 29ae505f36c1..d42b1ddbc1ad 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -20,8 +20,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.ADMIN_ID; -import static ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository.USER_ID; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; @Repository public class InMemoryMealRepository implements MealRepository { diff --git a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java similarity index 73% rename from src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java rename to src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index f8968a815041..d08419ab6aa3 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -1,6 +1,7 @@ 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; @@ -8,11 +9,18 @@ import java.util.List; import java.util.stream.Collectors; +import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER; + + @Repository public class InMemoryUserRepository extends InMemoryBaseRepository implements UserRepository { - static final int USER_ID = 1; - static final int ADMIN_ID = 2; + public void init() { + map.clear(); + map.put(UserTestData.USER_ID, USER); + map.put(UserTestData.ADMIN_ID, ADMIN); + } @Override public List getAll() { 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..d124b55574f1 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -0,0 +1,51 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.*; +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.InMemoryUserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Arrays; +import java.util.Collection; + +import static ru.javawebinar.topjava.UserTestData.ADMIN; + +public class InMemoryAdminRestControllerTest { + private static ConfigurableApplicationContext appCtx; + private static AdminRestController controller; + + @BeforeClass + public static void beforeClass() { + appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + System.out.println("\n" + Arrays.toString(appCtx.getBeanDefinitionNames()) + "\n"); + controller = appCtx.getBean(AdminRestController.class); + } + + @AfterClass + public static void afterClass() { + appCtx.close(); + } + + @Before + public void setUp() throws Exception { + // re-initialize + InMemoryUserRepository repository = appCtx.getBean(InMemoryUserRepository.class); + repository.init(); + } + + @Test + public void delete() throws Exception { + controller.delete(UserTestData.USER_ID); + Collection users = controller.getAll(); + Assert.assertEquals(users.size(), 1); + Assert.assertEquals(users.iterator().next(), ADMIN); + } + + @Test(expected = NotFoundException.class) + public void deleteNotFound() throws Exception { + controller.delete(10); + } +} \ No newline at end of file From 423f714fed8d0288b1e3e3a85e06fbcc4e963e47 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 13:15:27 +0300 Subject: [PATCH 40/97] Applied patch - 3_08_add_spring_test --- pom.xml | 6 +++ ...InMemoryAdminRestControllerSpringTest.java | 46 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java diff --git a/pom.xml b/pom.xml index 8f3fc31f9d77..970a6a767bcf 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,12 @@ ${junit.version} test + + org.springframework + spring-test + ${spring.version} + test + 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..28169834d554 --- /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.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Collection; + +import static ru.javawebinar.topjava.UserTestData.ADMIN; + +@ContextConfiguration("classpath:spring/spring-app.xml") +@RunWith(SpringRunner.class) +public class InMemoryAdminRestControllerSpringTest { + + @Autowired + private AdminRestController controller; + + @Autowired + private InMemoryUserRepository repository; + + @Before + public void setUp() throws Exception { + repository.init(); + } + + @Test + public void delete() throws Exception { + controller.delete(UserTestData.USER_ID); + Collection users = controller.getAll(); + Assert.assertEquals(users.size(), 1); + Assert.assertEquals(users.iterator().next(), ADMIN); + } + + @Test(expected = NotFoundException.class) + public void deleteNotFound() throws Exception { + controller.delete(10); + } +} From a5b23f3c1d0d19c49353de9b7199e4d7d6d0d9ed Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 16:16:38 +0300 Subject: [PATCH 41/97] Applied patch - 3_09_add_postgresql --- pom.xml | 9 +++++++++ src/main/resources/db/postgres.properties | 7 +++++++ 2 files changed, 16 insertions(+) create mode 100644 src/main/resources/db/postgres.properties diff --git a/pom.xml b/pom.xml index 970a6a767bcf..a0c9e73ce746 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,8 @@ 1.2.3 1.7.28 + + 42.2.8 4.12 @@ -73,6 +75,13 @@ ${spring.version} + + + org.postgresql + postgresql + ${postgresql.version} + + javax.servlet diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties new file mode 100644 index 000000000000..a3dcb93352b8 --- /dev/null +++ b/src/main/resources/db/postgres.properties @@ -0,0 +1,7 @@ +#database.url=jdbc:postgresql://ec2-54-247-74-197.eu-west-1.compute.amazonaws.com:5432/de4fjsqhdvl7ld?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl.NonValidatingFactory +#database.username=anbxkjtzukqacj +#database.password=da1f25b2a38784fb0d46858e5b8fc168e08c9e1e9c72faea5bbac9c0e1f9c24f + +database.url=jdbc:postgresql://localhost:5432/topjava +database.username=user +database.password=password From 6c76bd9b0def000b5ecfa4daa6f80b424ed784b6 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 16:57:34 +0300 Subject: [PATCH 42/97] Applied patch - 3_10_db_implementation --- pom.xml | 5 ++ .../repository/jdbc/JdbcUserRepository.java | 81 +++++++++++++++++++ src/main/resources/db/initDB.sql | 25 ++++++ src/main/resources/db/populateDB.sql | 11 +++ src/main/resources/spring/spring-db.xml | 25 ++++++ 5 files changed, 147 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java create mode 100644 src/main/resources/db/initDB.sql create mode 100644 src/main/resources/db/populateDB.sql create mode 100644 src/main/resources/spring/spring-db.xml diff --git a/pom.xml b/pom.xml index a0c9e73ce746..375a1c700725 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,11 @@ spring-context ${spring.version} + + org.springframework + spring-jdbc + ${spring.version} + diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java new file mode 100644 index 000000000000..7f6f8626a869 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -0,0 +1,81 @@ +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.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +@Repository +public class JdbcUserRepository implements UserRepository { + + private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertUser; + + @Autowired + public JdbcUserRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertUser = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("users") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @Override + public User save(User user) { + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", user.getId()) + .addValue("name", user.getName()) + .addValue("email", user.getEmail()) + .addValue("password", user.getPassword()) + .addValue("registered", user.getRegistered()) + .addValue("enabled", user.isEnabled()) + .addValue("caloriesPerDay", user.getCaloriesPerDay()); + + if (user.isNew()) { + Number newKey = insertUser.executeAndReturnKey(map); + user.setId(newKey.intValue()); + } else if (namedParameterJdbcTemplate.update( + "UPDATE users SET name=:name, email=:email, password=:password, " + + "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", map) == 0) { + return null; + } + return user; + } + + @Override + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0; + } + + @Override + public User get(int id) { + List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id); + return DataAccessUtils.singleResult(users); + } + + @Override + public User getByEmail(String email) { +// return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + return DataAccessUtils.singleResult(users); + } + + @Override + public List getAll() { + return jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + } +} diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql new file mode 100644 index 000000000000..dd49ab648475 --- /dev/null +++ b/src/main/resources/db/initDB.sql @@ -0,0 +1,25 @@ +DROP TABLE IF EXISTS user_roles; +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 +); \ 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..ccf86e060a68 --- /dev/null +++ b/src/main/resources/db/populateDB.sql @@ -0,0 +1,11 @@ +DELETE FROM user_roles; +DELETE FROM users; +ALTER SEQUENCE global_seq RESTART WITH 100000; + +INSERT INTO users (name, email, password) VALUES + ('User', 'user@yandex.ru', 'password'), + ('Admin', 'admin@gmail.com', 'admin'); + +INSERT INTO user_roles (role, user_id) VALUES + ('ROLE_USER', 100000), + ('ROLE_ADMIN', 100001); diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml new file mode 100644 index 000000000000..8ec56c9e63f0 --- /dev/null +++ b/src/main/resources/spring/spring-db.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 637d1181667c33a1c8d31e755a7ada208f158a71 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 17:39:37 +0300 Subject: [PATCH 43/97] Applied patch - 3_11_test_UserService --- pom.xml | 6 ++ .../topjava/model/AbstractBaseEntity.java | 22 +++++ .../topjava/model/AbstractNamedEntity.java | 3 + .../ru/javawebinar/topjava/model/User.java | 25 ++++-- .../repository/jdbc/JdbcMealRepository.java | 37 ++++++++ src/main/resources/spring/spring-app.xml | 2 +- .../ru/javawebinar/topjava/UserTestData.java | 21 ++++- .../topjava/service/UserServiceTest.java | 87 +++++++++++++++++++ 8 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java diff --git a/pom.xml b/pom.xml index 375a1c700725..2c12de1791a8 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,12 @@ ${spring.version} test + + org.assertj + assertj-core + 3.13.2 + test + diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index 8f27c902e047..46e8d6e3038d 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,8 +1,13 @@ package ru.javawebinar.topjava.model; public abstract class AbstractBaseEntity { + public static final int START_SEQ = 100000; + protected Integer id; + public AbstractBaseEntity() { + } + protected AbstractBaseEntity(Integer id) { this.id = id; } @@ -23,4 +28,21 @@ public boolean isNew() { public String toString() { return getClass().getSimpleName() + ":" + id; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + 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 index 2054a3d3cf9b..5f0fde2bbfa8 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -4,6 +4,9 @@ public abstract class AbstractNamedEntity extends AbstractBaseEntity { protected String name; + public AbstractNamedEntity() { + } + protected AbstractNamedEntity(Integer id, String name) { super(id); this.name = name; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index d88e381945d8..446d86ffef6a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,5 +1,8 @@ package ru.javawebinar.topjava.model; +import org.springframework.util.CollectionUtils; + +import java.util.Collection; import java.util.Date; import java.util.EnumSet; import java.util.Set; @@ -20,17 +23,25 @@ public class User extends AbstractNamedEntity { private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + 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, Role role, Role... roles) { - this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, EnumSet.of(role, roles)); + this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), EnumSet.of(role, roles)); } - public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Set 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.roles = roles; + this.registered = registered; + setRoles(roles); } public String getEmail() { @@ -77,15 +88,19 @@ public String getPassword() { return password; } + public void setRoles(Collection roles) { + this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles); + } + @Override public String toString() { - return "User (" + + 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/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java new file mode 100644 index 000000000000..f431595681fd --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class JdbcMealRepository implements MealRepository { + + @Override + public Meal save(Meal meal, int userId) { + return null; + } + + @Override + public boolean delete(int id, int userId) { + return false; + } + + @Override + public Meal get(int id, int userId) { + return null; + } + + @Override + public List getAll(int userId) { + return null; + } + + @Override + public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + return null; + } +} diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 31a70253bf5a..a66de2443808 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -15,7 +15,7 @@ - + diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index 196b09362719..3cc466c53754 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -3,10 +3,27 @@ import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + public class UserTestData { - public static final int USER_ID = 1; - public static final int ADMIN_ID = 2; + 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", Role.ROLE_USER); public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ROLE_ADMIN); + + public static void assertMatch(User actual, User expected) { + assertThat(actual).isEqualToIgnoringGivenFields(expected, "registered", "roles"); + } + + public static void assertMatch(Iterable actual, User... expected) { + assertMatch(actual, Arrays.asList(expected)); + } + + public static void assertMatch(Iterable actual, Iterable expected) { + assertThat(actual).usingElementComparatorIgnoringFields("registered", "roles").isEqualTo(expected); + } } diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java new file mode 100644 index 000000000000..dc9ad49482f3 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -0,0 +1,87 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +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 ru.javawebinar.topjava.UserTestData.*; + +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-db.xml" +}) +@RunWith(SpringRunner.class) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +public class UserServiceTest { + + @Autowired + private UserService service; + + @Test + public 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(newUser); + newUser.setId(created.getId()); + assertMatch(service.getAll(), ADMIN, newUser, USER); + } + + @Test(expected = DataAccessException.class) + public void duplicateMailCreate() throws Exception { + service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.ROLE_USER)); + } + + @Test + public void delete() throws Exception { + service.delete(USER_ID); + assertMatch(service.getAll(), ADMIN); + } + + @Test(expected = NotFoundException.class) + public void deletedNotFound() throws Exception { + service.delete(1); + } + + @Test + public void get() throws Exception { + User user = service.get(USER_ID); + assertMatch(user, USER); + } + + @Test(expected = NotFoundException.class) + public void getNotFound() throws Exception { + service.get(1); + } + + @Test + public void getByEmail() throws Exception { + User user = service.getByEmail("user@yandex.ru"); + assertMatch(user, USER); + } + + @Test + public void update() throws Exception { + User updated = new User(USER); + updated.setName("UpdatedName"); + updated.setCaloriesPerDay(330); + service.update(updated); + assertMatch(service.get(USER_ID), updated); + } + + @Test + public void getAll() throws Exception { + List all = service.getAll(); + assertMatch(all, ADMIN, USER); + } +} \ No newline at end of file From ee7af0bcf0338e11d3095e95629e78677f19ab17 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 17:40:08 +0300 Subject: [PATCH 44/97] Applied patch - 3_12_test_logging --- pom.xml | 7 ++++++ src/main/resources/logback.xml | 4 ++-- .../topjava/service/UserServiceTest.java | 7 ++++++ src/test/resources/logback-test.xml | 22 +++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/logback-test.xml diff --git a/pom.xml b/pom.xml index 2c12de1791a8..a667ec8918bb 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,13 @@ compile + + org.slf4j + jul-to-slf4j + ${slf4j.version} + runtime + + ch.qos.logback logback-classic diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index e9b900b26669..c7bffc3a958c 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -9,14 +9,14 @@ UTF-8 - %date %-5level %logger{0} [%file:%line] %msg%n + %date %-5level %logger{50}.%M:%L - %msg%n UTF-8 - %-5level %logger{0} [%file:%line] %msg%n + %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index dc9ad49482f3..03bfc5f2e6ac 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -2,6 +2,7 @@ import org.junit.Test; import org.junit.runner.RunWith; +import org.slf4j.bridge.SLF4JBridgeHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.test.context.ContextConfiguration; @@ -26,6 +27,12 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) public class UserServiceTest { + static { + // Only for postgres driver logging + // It uses java.util.logging and logged via jul-to-slf4j bridge + SLF4JBridgeHandler.install(); + } + @Autowired private UserService service; diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..428ceca2ee02 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,22 @@ + + + + true + + + + + UTF-8 + %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n + + + + + + + + + + + + \ No newline at end of file From 0d0b6b608339f9d4429a05b69ff7f3ec7b9f7898 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 18 Oct 2019 17:40:33 +0300 Subject: [PATCH 45/97] Applied patch - 3_13_fix_servlet --- src/main/java/ru/javawebinar/topjava/web/MealServlet.java | 2 +- src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java | 4 +++- src/main/webapp/index.html | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index ec26cd4021a7..919f3888d584 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -29,7 +29,7 @@ public class MealServlet extends HttpServlet { @Override public void init(ServletConfig config) throws ServletException { super.init(config); - springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/spring-db.xml"); mealController = springContext.getBean(MealRestController.class); } diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java index b9639bf1b9ea..588217547e60 100644 --- a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -1,10 +1,12 @@ package ru.javawebinar.topjava.web; +import ru.javawebinar.topjava.model.AbstractBaseEntity; + import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; public class SecurityUtil { - private static int id = 1; + private static int id = AbstractBaseEntity.START_SEQ; public static int authUserId() { return id; diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index e68aaa192cd3..886449733a86 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -9,8 +9,8 @@

    Проект Meals of  From c7e34cce9d4ad58b3f7a7fe694becaeacc5aca6e Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 11:54:07 +0300 Subject: [PATCH 46/97] Applied patch - 4_0_fix --- .../ru/javawebinar/topjava/service/MealService.java | 3 ++- .../repository/inmemory/InMemoryBaseRepository.java | 4 +++- .../user/InMemoryAdminRestControllerSpringTest.java | 4 ++-- .../web/user/InMemoryAdminRestControllerTest.java | 10 +++++++--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 1e433b1d5158..75c5ab0f37b0 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -32,7 +32,8 @@ public void delete(int id, int userId) { public List getBetweenDates(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { return repository.getBetween( DateTimeUtil.createDateTime(startDate, LocalDate.MIN, LocalTime.MIN), - DateTimeUtil.createDateTime(endDate, LocalDate.MAX, LocalTime.MAX), userId); + DateTimeUtil.createDateTime(endDate, LocalDate.MAX, LocalTime.MAX), + userId); } public List getAll(int userId) { diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java index 0b6a84c7f2ec..a49a4fbdefba 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -8,10 +8,12 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + @Repository public class InMemoryBaseRepository { - private static AtomicInteger counter = new AtomicInteger(0); + private static AtomicInteger counter = new AtomicInteger(START_SEQ); Map map = new ConcurrentHashMap<>(); diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java index 28169834d554..be762bf3bc3b 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -35,8 +35,8 @@ public void setUp() throws Exception { public void delete() throws Exception { controller.delete(UserTestData.USER_ID); Collection users = controller.getAll(); - Assert.assertEquals(users.size(), 1); - Assert.assertEquals(users.iterator().next(), ADMIN); + Assert.assertEquals(1, users.size()); + Assert.assertEquals(ADMIN, users.iterator().next()); } @Test(expected = NotFoundException.class) diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index d124b55574f1..7c769e4ccce7 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.web.user; import org.junit.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import ru.javawebinar.topjava.UserTestData; @@ -14,13 +16,15 @@ import static ru.javawebinar.topjava.UserTestData.ADMIN; public class InMemoryAdminRestControllerTest { + private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); + private static ConfigurableApplicationContext appCtx; private static AdminRestController controller; @BeforeClass public static void beforeClass() { appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); - System.out.println("\n" + Arrays.toString(appCtx.getBeanDefinitionNames()) + "\n"); + log.info("\n{}\n", Arrays.toString(appCtx.getBeanDefinitionNames())); controller = appCtx.getBean(AdminRestController.class); } @@ -40,8 +44,8 @@ public void setUp() throws Exception { public void delete() throws Exception { controller.delete(UserTestData.USER_ID); Collection users = controller.getAll(); - Assert.assertEquals(users.size(), 1); - Assert.assertEquals(users.iterator().next(), ADMIN); + Assert.assertEquals(1, users.size()); + Assert.assertEquals(ADMIN, users.iterator().next()); } @Test(expected = NotFoundException.class) From 4069e65f82356a497b2125c7355ed243f6c23ebf Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 11:56:02 +0300 Subject: [PATCH 47/97] Applied patch - 4_1_HW3 --- .../ru/javawebinar/topjava/model/Meal.java | 21 ++++++- .../repository/jdbc/JdbcMealRepository.java | 60 +++++++++++++++++-- .../repository/jdbc/JdbcUserRepository.java | 15 ++--- src/main/resources/db/initDB.sql | 14 ++++- src/main/resources/db/populateDB.sql | 11 ++++ 5 files changed, 101 insertions(+), 20 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 9eed15f706be..1c1e67b15a3b 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -5,11 +5,14 @@ import java.time.LocalTime; public class Meal extends AbstractBaseEntity { - private final LocalDateTime dateTime; + private LocalDateTime dateTime; - private final String description; + private String description; - private final int calories; + private int calories; + + public Meal() { + } public Meal(LocalDateTime dateTime, String description, int calories) { this(null, dateTime, description, calories); @@ -42,6 +45,18 @@ public LocalTime getTime() { return dateTime.toLocalTime(); } + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setCalories(int calories) { + this.calories = calories; + } + @Override public String toString() { return "Meal{" + diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index f431595681fd..fec32a8cca5b 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -1,5 +1,13 @@ package ru.javawebinar.topjava.repository.jdbc; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; @@ -10,28 +18,70 @@ @Repository public class JdbcMealRepository implements MealRepository { + private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertMeal; + + @Autowired + public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("meals") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + @Override public Meal save(Meal meal, int userId) { - return null; + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", meal.getId()) + .addValue("description", meal.getDescription()) + .addValue("calories", meal.getCalories()) + .addValue("date_time", meal.getDateTime()) + .addValue("user_id", userId); + + if (meal.isNew()) { + Number newId = insertMeal.executeAndReturnKey(map); + meal.setId(newId.intValue()); + } else { + if (namedParameterJdbcTemplate.update("" + + "UPDATE meals " + + " SET description=:description, calories=:calories, date_time=:date_time " + + " WHERE id=:id AND user_id=:user_id" + , map) == 0) { + return null; + } + } + return meal; } @Override public boolean delete(int id, int userId) { - return false; + return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0; } @Override public Meal get(int id, int userId) { - return null; + 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 null; + 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 null; + 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/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java index 7f6f8626a869..412bfbebc5df 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -4,7 +4,7 @@ import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; @@ -36,21 +36,14 @@ public JdbcUserRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate @Override public User save(User user) { - MapSqlParameterSource map = new MapSqlParameterSource() - .addValue("id", user.getId()) - .addValue("name", user.getName()) - .addValue("email", user.getEmail()) - .addValue("password", user.getPassword()) - .addValue("registered", user.getRegistered()) - .addValue("enabled", user.isEnabled()) - .addValue("caloriesPerDay", user.getCaloriesPerDay()); + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user); if (user.isNew()) { - Number newKey = insertUser.executeAndReturnKey(map); + Number newKey = insertUser.executeAndReturnKey(parameterSource); user.setId(newKey.intValue()); } else if (namedParameterJdbcTemplate.update( "UPDATE users SET name=:name, email=:email, password=:password, " + - "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", map) == 0) { + "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", parameterSource) == 0) { return null; } return user; diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql index dd49ab648475..f39e3d5c3294 100644 --- a/src/main/resources/db/initDB.sql +++ b/src/main/resources/db/initDB.sql @@ -1,4 +1,5 @@ DROP TABLE IF EXISTS user_roles; +DROP TABLE IF EXISTS meals; DROP TABLE IF EXISTS users; DROP SEQUENCE IF EXISTS global_seq; @@ -22,4 +23,15 @@ CREATE TABLE user_roles role VARCHAR, CONSTRAINT user_roles_idx UNIQUE (user_id, role), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE -); \ No newline at end of file +); + +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/populateDB.sql b/src/main/resources/db/populateDB.sql index ccf86e060a68..29a97efdc2b8 100644 --- a/src/main/resources/db/populateDB.sql +++ b/src/main/resources/db/populateDB.sql @@ -1,4 +1,5 @@ DELETE FROM user_roles; +DELETE FROM meals; DELETE FROM users; ALTER SEQUENCE global_seq RESTART WITH 100000; @@ -9,3 +10,13 @@ INSERT INTO users (name, email, password) VALUES INSERT INTO user_roles (role, user_id) VALUES ('ROLE_USER', 100000), ('ROLE_ADMIN', 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); From bb01feface13966c1bc0cd77aca85623b88d46ab Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 11:57:42 +0300 Subject: [PATCH 48/97] Applied patch - 4_2_HW3_optional --- .../javawebinar/topjava/util/MealsUtil.java | 12 --- .../ru/javawebinar/topjava/MealTestData.java | 47 ++++++++ .../inmemory/InMemoryMealRepository.java | 13 --- .../topjava/service/MealServiceTest.java | 100 ++++++++++++++++++ 4 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/MealTestData.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 5a35ba6ff1a3..f1a31e024adc 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -5,10 +5,7 @@ import ru.javawebinar.topjava.to.MealTo; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.Month; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -18,15 +15,6 @@ public class MealsUtil { public static final int DEFAULT_CALORIES_PER_DAY = 2000; - public static final List MEALS = Arrays.asList( - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 1000), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) - ); - public static List getTos(Collection meals, int caloriesPerDay) { return getFiltered(meals, caloriesPerDay, meal -> true); } 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..d94faebff1c4 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -0,0 +1,47 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Meal; + +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +import static java.time.LocalDateTime.of; +import static org.assertj.core.api.Assertions.assertThat; +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 = Arrays.asList(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).isEqualToComparingFieldByField(expected); + } + + public static void assertMatch(Iterable actual, Meal... expected) { + assertMatch(actual, Arrays.asList(expected)); + } + + public static void assertMatch(Iterable actual, Iterable expected) { + assertThat(actual).usingFieldByFieldElementComparator().isEqualTo(expected); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index d42b1ddbc1ad..e08050d9e492 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -5,13 +5,11 @@ import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; -import ru.javawebinar.topjava.util.MealsUtil; import ru.javawebinar.topjava.util.Util; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.time.LocalDateTime; -import java.time.Month; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -20,9 +18,6 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; -import static ru.javawebinar.topjava.UserTestData.USER_ID; - @Repository public class InMemoryMealRepository implements MealRepository { private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepository.class); @@ -30,14 +25,6 @@ public class InMemoryMealRepository implements MealRepository { // Map userId -> mealRepository private Map> usersMealsMap = new ConcurrentHashMap<>(); - { - MealsUtil.MEALS.forEach(meal -> save(meal, USER_ID)); - - save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510), ADMIN_ID); - save(new Meal(LocalDateTime.of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500), ADMIN_ID); - } - - @Override public Meal save(Meal meal, int userId) { InMemoryBaseRepository meals = usersMealsMap.computeIfAbsent(userId, uid -> new InMemoryBaseRepository<>()); diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java new file mode 100644 index 000000000000..23693e8dfe22 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -0,0 +1,100 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.bridge.SLF4JBridgeHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.time.LocalDate; +import java.time.Month; + +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-db.xml" +}) +@RunWith(SpringJUnit4ClassRunner.class) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +public class MealServiceTest { + + static { + SLF4JBridgeHandler.install(); + } + + @Autowired + private MealService service; + + @Test + public void delete() throws Exception { + service.delete(MEAL1_ID, USER_ID); + assertMatch(service.getAll(USER_ID), MEAL6, MEAL5, MEAL4, MEAL3, MEAL2); + } + + @Test(expected = NotFoundException.class) + public void deleteNotFound() throws Exception { + service.delete(1, USER_ID); + } + + @Test(expected = NotFoundException.class) + public void deleteNotOwn() throws Exception { + service.delete(MEAL1_ID, ADMIN_ID); + } + + @Test + public 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 + public void get() throws Exception { + Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); + assertMatch(actual, ADMIN_MEAL1); + } + + @Test(expected = NotFoundException.class) + public void getNotFound() throws Exception { + service.get(1, USER_ID); + } + + @Test(expected = NotFoundException.class) + public void getNotOwn() throws Exception { + service.get(MEAL1_ID, ADMIN_ID); + } + + @Test + public void update() throws Exception { + Meal updated = getUpdated(); + service.update(updated, USER_ID); + assertMatch(service.get(MEAL1_ID, USER_ID), updated); + } + + @Test(expected = NotFoundException.class) + public void updateNotFound() throws Exception { + service.update(MEAL1, ADMIN_ID); + } + + @Test + public void getAll() throws Exception { + assertMatch(service.getAll(USER_ID), MEALS); + } + + @Test + public void getBetween() throws Exception { + assertMatch(service.getBetweenDates( + LocalDate.of(2015, Month.MAY, 30), + LocalDate.of(2015, Month.MAY, 30), USER_ID), MEAL3, MEAL2, MEAL1); + } +} \ No newline at end of file From 8ea1e9a02b43903067cc2d6f6441b7182d834e1c Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 11:58:10 +0300 Subject: [PATCH 49/97] Applied patch - 4_3_HW3_fix_logging --- src/main/resources/spring/spring-db.xml | 9 +++++++++ .../ru/javawebinar/topjava/service/MealServiceTest.java | 5 ----- .../ru/javawebinar/topjava/service/UserServiceTest.java | 7 ------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 8ec56c9e63f0..283fee39434a 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -6,6 +6,15 @@ + + + + + diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index 23693e8dfe22..46f9288d5238 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -2,7 +2,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.slf4j.bridge.SLF4JBridgeHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.jdbc.Sql; @@ -26,10 +25,6 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) public class MealServiceTest { - static { - SLF4JBridgeHandler.install(); - } - @Autowired private MealService service; diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index 03bfc5f2e6ac..dc9ad49482f3 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -2,7 +2,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.slf4j.bridge.SLF4JBridgeHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.test.context.ContextConfiguration; @@ -27,12 +26,6 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) public class UserServiceTest { - static { - // Only for postgres driver logging - // It uses java.util.logging and logged via jul-to-slf4j bridge - SLF4JBridgeHandler.install(); - } - @Autowired private UserService service; From d933da8255a0169527d18e62cd632ef2acc590a6 Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 12:25:01 +0300 Subject: [PATCH 50/97] Applied patch - 4_4_improve_code --- .codacy.yml | 5 ++++ .travis.yml | 26 +++++++++++++++++++ .../topjava/service/MealService.java | 3 +++ .../topjava/service/UserService.java | 4 +++ .../topjava/util/DateTimeUtil.java | 3 +++ .../javawebinar/topjava/util/MealsUtil.java | 3 +++ .../ru/javawebinar/topjava/util/Util.java | 3 +++ .../topjava/util/ValidationUtil.java | 3 +++ .../javawebinar/topjava/web/SecurityUtil.java | 3 +++ .../inmemory/InMemoryBaseRepository.java | 2 ++ .../inmemory/InMemoryMealRepository.java | 8 +++--- .../inmemory/InMemoryUserRepository.java | 2 ++ 12 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 .codacy.yml create mode 100644 .travis.yml diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 000000000000..9adc006ee26b --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,5 @@ +--- +exclude_paths: + - 'src/main/webapp/**' + - 'README.md' + - '**.sql' \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000000..1cc16498abdd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +# https://docs.travis-ci.com/user/languages/java/ +language: java +jdk: openjdk8 + +#https://dzone.com/articles/travis-ci-tutorial-java-projects +cache: + directories: + - $HOME/.m2 + +# https://docs.travis-ci.com/user/database-setup/#PostgreSQL +before_script: +- psql -c 'create database topjava' -U postgres +- psql -c 'create user "user"; grant all privileges on database topjava to "user"' -U postgres + +# https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches +branches: + only: + - master + +# https://stackoverflow.com/a/49852690/548473: +services: + - postgresql + +# https://docs.travis-ci.com/user/notifications#Configuring-email-notifications +#notifications: +# email: false \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 75c5ab0f37b0..7d5c9653be1a 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -2,6 +2,7 @@ import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.DateTimeUtil; @@ -41,10 +42,12 @@ public List getAll(int userId) { } public void update(Meal meal, int userId) { + Assert.notNull(meal, "meal must not be null"); checkNotFoundWithId(repository.save(meal, userId), meal.getId()); } public Meal create(Meal meal, int userId) { + Assert.notNull(meal, "meal must not be null"); return repository.save(meal, userId); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 5a7527bddabe..d9811b3abda7 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -2,6 +2,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -22,6 +23,7 @@ public UserService(UserRepository repository) { } public User create(User user) { + Assert.notNull(user, "user must not be null"); return repository.save(user); } @@ -34,6 +36,7 @@ public User get(int id) throws NotFoundException { } public User getByEmail(String email) throws NotFoundException { + Assert.notNull(email, "email must not be null"); return checkNotFound(repository.getByEmail(email), "email=" + email); } @@ -42,6 +45,7 @@ public List getAll() { } public void update(User user) throws NotFoundException { + Assert.notNull(user, "user must not be null"); checkNotFoundWithId(repository.save(user), user.getId()); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 5f6f3f82e628..e8e0c323385b 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -11,6 +11,9 @@ public class DateTimeUtil { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + private DateTimeUtil() { + } + public static String toString(LocalDateTime ldt) { return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); } diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index f1a31e024adc..1f9d58e288d1 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -15,6 +15,9 @@ public class MealsUtil { public static final int DEFAULT_CALORIES_PER_DAY = 2000; + private MealsUtil() { + } + public static List getTos(Collection meals, int caloriesPerDay) { return getFiltered(meals, caloriesPerDay, meal -> true); } diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java index bea8af11cb74..77b675a2b1cd 100644 --- a/src/main/java/ru/javawebinar/topjava/util/Util.java +++ b/src/main/java/ru/javawebinar/topjava/util/Util.java @@ -3,6 +3,9 @@ import org.springframework.lang.Nullable; public class Util { + private Util() { + } + public static > boolean isBetweenInclusive(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 index f38a2d255603..cb51f3c60ac3 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -6,6 +6,9 @@ public class ValidationUtil { + private ValidationUtil() { + } + public static T checkNotFoundWithId(T object, int id) { return checkNotFound(object, "id=" + id); } diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java index 588217547e60..4bad5863e3c6 100644 --- a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -8,6 +8,9 @@ public class SecurityUtil { private static int id = AbstractBaseEntity.START_SEQ; + private SecurityUtil() { + } + public static int authUserId() { return id; } diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java index a49a4fbdefba..5b88d9298b63 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -18,6 +19,7 @@ public class InMemoryBaseRepository { Map map = new ConcurrentHashMap<>(); public T save(T entry) { + Objects.requireNonNull(entry, "Entry must not be null"); if (entry.isNew()) { entry.setId(counter.incrementAndGet()); map.put(entry.getId(), entry); diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index e08050d9e492..f8df320023dc 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -10,10 +10,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.time.LocalDateTime; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -27,6 +24,7 @@ public class InMemoryMealRepository implements MealRepository { @Override public Meal save(Meal meal, int userId) { + Objects.requireNonNull(meal, "meal must not be null"); InMemoryBaseRepository meals = usersMealsMap.computeIfAbsent(userId, uid -> new InMemoryBaseRepository<>()); return meals.save(meal); } @@ -60,6 +58,8 @@ public List getAll(int userId) { @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.isBetweenInclusive(meal.getDateTime(), startDateTime, endDateTime)); } diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java index d08419ab6aa3..e615f964e40e 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -7,6 +7,7 @@ import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import static ru.javawebinar.topjava.UserTestData.ADMIN; @@ -31,6 +32,7 @@ public List getAll() { @Override public User getByEmail(String email) { + Objects.requireNonNull(email, "email must not be null"); return getCollection().stream() .filter(u -> email.equals(u.getEmail())) .findFirst() From 2d1ac4a564a77fe8375218b110b6bc8d0c4e6d0c Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 12:27:34 +0300 Subject: [PATCH 51/97] Applied patch - 4_5_init_and_populate_db --- src/main/resources/db/postgres.properties | 1 + src/main/resources/spring/spring-db.xml | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index a3dcb93352b8..e5aeb857eb9a 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -5,3 +5,4 @@ database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password +database.init=true \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 283fee39434a..72a3f6178c3e 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -1,8 +1,10 @@ + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> @@ -15,6 +17,11 @@ + + + + + From 619b4fcc37b73e2b6254b411aec6288fc12017a5 Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 12:27:59 +0300 Subject: [PATCH 52/97] Applied patch - 4_6_create_inmemory_test_ctx --- src/main/resources/spring/spring-app.xml | 2 -- src/main/resources/spring/spring-db.xml | 2 ++ src/test/java/ru/javawebinar/topjava/SpringMain.java | 2 +- .../web/user/InMemoryAdminRestControllerSpringTest.java | 2 +- .../topjava/web/user/InMemoryAdminRestControllerTest.java | 2 +- src/test/resources/spring/inmemory.xml | 7 +++++++ 6 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 src/test/resources/spring/inmemory.xml diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index a66de2443808..b220a8ba3bf4 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -15,8 +15,6 @@ - - diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 72a3f6178c3e..8e33d32f5f49 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -17,6 +17,8 @@ + + diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java index f72561bbd4b8..15187b1f72f1 100644 --- a/src/test/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -17,7 +17,7 @@ public class SpringMain { public static void main(String[] args) { // java 7 automatic resource management - try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml")) { + try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml")) { System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ROLE_ADMIN)); diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java index be762bf3bc3b..b535e7c261f1 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -16,7 +16,7 @@ import static ru.javawebinar.topjava.UserTestData.ADMIN; -@ContextConfiguration("classpath:spring/spring-app.xml") +@ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) @RunWith(SpringRunner.class) public class InMemoryAdminRestControllerSpringTest { diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index 7c769e4ccce7..bf89efacfe52 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -23,7 +23,7 @@ public class InMemoryAdminRestControllerTest { @BeforeClass public static void beforeClass() { - appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml"); log.info("\n{}\n", Arrays.toString(appCtx.getBeanDefinitionNames())); controller = appCtx.getBean(AdminRestController.class); } diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml new file mode 100644 index 000000000000..c6a2710cbd88 --- /dev/null +++ b/src/test/resources/spring/inmemory.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file From 2d20bbe4229c26cc2d06fb458e5193bb0dcb8899 Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 12:28:27 +0300 Subject: [PATCH 53/97] Applied patch - 4_7_add_jpa --- pom.xml | 27 +++++++- .../topjava/model/AbstractBaseEntity.java | 10 ++- .../topjava/model/AbstractNamedEntity.java | 12 +++- .../ru/javawebinar/topjava/model/Meal.java | 13 ++++ .../ru/javawebinar/topjava/model/User.java | 24 ++++++++ .../repository/jpa/JpaMealRepository.java | 37 +++++++++++ .../repository/jpa/JpaUserRepository.java | 61 +++++++++++++++++++ src/main/resources/db/postgres.properties | 6 +- src/main/resources/spring/spring-db.xml | 36 +++++++++-- 9 files changed, 216 insertions(+), 10 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java diff --git a/pom.xml b/pom.xml index a667ec8918bb..7433df74702d 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,11 @@ 42.2.8 4.12 + + + 5.4.7.Final + 6.0.17.Final + 3.0.1-b11 @@ -83,7 +88,7 @@ org.springframework - spring-jdbc + spring-orm ${spring.version} @@ -94,6 +99,26 @@ ${postgresql.version} + + + org.hibernate + hibernate-core + ${hibernate.version} + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + + + org.glassfish + javax.el + ${javax-el.version} + provided + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index 46e8d6e3038d..acee5c76ee5d 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,11 +1,19 @@ package ru.javawebinar.topjava.model; +import javax.persistence.*; + +@MappedSuperclass +// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access +@Access(AccessType.FIELD) public abstract class AbstractBaseEntity { public static final int START_SEQ = 100000; + @Id + @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") protected Integer id; - public AbstractBaseEntity() { + protected AbstractBaseEntity() { } protected AbstractBaseEntity(Integer id) { diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java index 5f0fde2bbfa8..edff2ab1718f 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -1,10 +1,20 @@ package ru.javawebinar.topjava.model; +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + + +@MappedSuperclass public abstract class AbstractNamedEntity extends AbstractBaseEntity { + @NotBlank + @Size(min = 2, max = 100) + @Column(name = "name", nullable = false) protected String name; - public AbstractNamedEntity() { + protected AbstractNamedEntity() { } protected AbstractNamedEntity(Integer id, String name) { diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 1c1e67b15a3b..788ae8f6fe5b 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.model; +import javax.persistence.FetchType; +import javax.persistence.ManyToOne; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; @@ -11,6 +13,9 @@ public class Meal extends AbstractBaseEntity { private int calories; + @ManyToOne(fetch = FetchType.LAZY) + private User user; + public Meal() { } @@ -57,6 +62,14 @@ public void setCalories(int calories) { this.calories = calories; } + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + @Override public String toString() { return "Meal{" + diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 446d86ffef6a..a6bd326f8080 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,7 +1,13 @@ package ru.javawebinar.topjava.model; +import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.util.Collection; import java.util.Date; import java.util.EnumSet; @@ -9,18 +15,36 @@ import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; +@Entity +@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "users_unique_email_idx")}) public class User extends AbstractNamedEntity { + @Column(name = "email", nullable = false, unique = true) + @Email + @NotBlank + @Size(max = 100) private String email; + @Column(name = "password", nullable = false) + @NotBlank + @Size(min = 5, max = 100) private String password; + @Column(name = "enabled", nullable = false, columnDefinition = "bool default true") private boolean enabled = true; + @Column(name = "registered", nullable = false, columnDefinition = "timestamp default now()") + @NotNull private Date registered = new Date(); + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) private Set roles; + @Column(name = "calories_per_day", nullable = false, columnDefinition = "int default 2000") + @Range(min = 10, max = 10000) private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; public User() { diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java new file mode 100644 index 000000000000..e6fb632419b7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class JpaMealRepository implements MealRepository { + + @Override + public Meal save(Meal meal, int userId) { + return null; + } + + @Override + public boolean delete(int id, int userId) { + return false; + } + + @Override + public Meal get(int id, int userId) { + return null; + } + + @Override + public List getAll(int userId) { + return null; + } + + @Override + public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java new file mode 100644 index 000000000000..68ae67d0152e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -0,0 +1,61 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import java.util.List; + +@Repository +public class JpaUserRepository implements UserRepository { + +/* + @Autowired + private SessionFactory sessionFactory; + + private Session openSession() { + return sessionFactory.getCurrentSession(); + } +*/ + + @PersistenceContext + private EntityManager em; + + @Override + 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 + 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; + } + + @Override + public User getByEmail(String email) { + return null; + } + + @Override + public List getAll() { + return null; + } +} diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index e5aeb857eb9a..040ea60c00ab 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -5,4 +5,8 @@ database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password -database.init=true \ No newline at end of file + +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/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 8e33d32f5f49..959e712ad856 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -1,5 +1,6 @@ - + @@ -33,11 +34,34 @@ - - - + + + + + + + + + + - - + + + + + + \ No newline at end of file From 35fb4d1e87109f696adb0225c4ec86a20198caae Mon Sep 17 00:00:00 2001 From: alkone Date: Mon, 28 Oct 2019 12:29:03 +0300 Subject: [PATCH 54/97] Applied patch - 4_8_add_named_query_and_transaction --- .../ru/javawebinar/topjava/model/User.java | 9 +++++++++ .../repository/jpa/JpaUserRepository.java | 19 +++++++++++++++---- src/main/resources/spring/spring-db.xml | 10 +++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index a6bd326f8080..328cfc71869d 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -15,10 +15,19 @@ import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; +@NamedQueries({ + @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), + @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), + @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email"), +}) @Entity @Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "users_unique_email_idx")}) public class User extends AbstractNamedEntity { + public static final String DELETE = "User.delete"; + public static final String BY_EMAIL = "User.getByEmail"; + public static final String ALL_SORTED = "User.getAllSorted"; + @Column(name = "email", nullable = false, unique = true) @Email @NotBlank diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java index 68ae67d0152e..9ade13354ac1 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -1,15 +1,17 @@ package ru.javawebinar.topjava.repository.jpa; +import org.springframework.dao.support.DataAccessUtils; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import javax.persistence.Query; import java.util.List; @Repository +@Transactional(readOnly = true) public class JpaUserRepository implements UserRepository { /* @@ -25,6 +27,7 @@ private Session openSession() { private EntityManager em; @Override + @Transactional public User save(User user) { if (user.isNew()) { em.persist(user); @@ -40,22 +43,30 @@ public User get(int 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) { - return null; + List users = em.createNamedQuery(User.BY_EMAIL, User.class) + .setParameter(1, email) + .getResultList(); + return DataAccessUtils.singleResult(users); } @Override public List getAll() { - return null; + return em.createNamedQuery(User.ALL_SORTED, User.class).getResultList(); } } diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 959e712ad856..fc45d2a4a5e0 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -3,9 +3,11 @@ xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd - http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> + http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> @@ -54,6 +56,12 @@ + + + + + diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties new file mode 100644 index 000000000000..6371e7b8073e --- /dev/null +++ b/src/main/resources/db/hsqldb.properties @@ -0,0 +1,12 @@ +#database.url=jdbc:hsqldb:file:D:/temp/topjava + +database.url=jdbc:hsqldb:mem:topjava +database.username=sa +database.password= +database.driverClassName=org.hsqldb.jdbcDriver + +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_hsql.sql b/src/main/resources/db/initDB_hsql.sql new file mode 100644 index 000000000000..8306d5157829 --- /dev/null +++ b/src/main/resources/db/initDB_hsql.sql @@ -0,0 +1,39 @@ +DROP TABLE user_roles IF EXISTS; +DROP TABLE meals IF EXISTS; +DROP TABLE users IF EXISTS; +DROP SEQUENCE global_seq IF EXISTS; + +CREATE SEQUENCE GLOBAL_SEQ AS INTEGER START WITH 100000; + +CREATE TABLE users +( + id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + registered TIMESTAMP DEFAULT now() NOT NULL, + enabled BOOLEAN DEFAULT TRUE NOT NULL, + calories_per_day INTEGER DEFAULT 2000 NOT NULL +); +CREATE UNIQUE INDEX users_unique_email_idx + ON USERS (email); + +CREATE TABLE user_roles +( + user_id INTEGER NOT NULL, + role VARCHAR(255), + CONSTRAINT user_roles_idx UNIQUE (user_id, role), + FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE +); + +CREATE TABLE meals +( + id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, + date_time TIMESTAMP NOT NULL, + description VARCHAR(255) NOT NULL, + calories INT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX meals_unique_user_datetime_idx + ON meals (user_id, date_time) \ No newline at end of file diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index 040ea60c00ab..f9a174a19897 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -5,8 +5,10 @@ database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password +database.driverClassName=org.postgresql.Driver database.init=true +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/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index fc45d2a4a5e0..71a6b17771f6 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -9,7 +9,10 @@ http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> + + + - + From f9915a1df6929268d527012b2441c98c95216e75 Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:24:47 +0300 Subject: [PATCH 56/97] Applied patch - 5_0_1_fix_tests --- src/main/resources/db/populateDB.sql | 1 + .../ru/javawebinar/topjava/MealTestData.java | 11 ++++++----- .../inmemory/InMemoryMealRepository.java | 8 ++++++++ .../topjava/service/MealServiceTest.java | 16 +++++++++++----- .../topjava/service/UserServiceTest.java | 10 ++++++---- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql index 29a97efdc2b8..3a9ed84b9cdf 100644 --- a/src/main/resources/db/populateDB.sql +++ b/src/main/resources/db/populateDB.sql @@ -15,6 +15,7 @@ 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 0:00:00', 'Еда на граничное значение', 100, 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), diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index d94faebff1c4..61daeaaf34e9 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -12,18 +12,19 @@ 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 int ADMIN_MEAL_ID = START_SEQ + 9; 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 MEAL4 = new Meal(MEAL1_ID + 3, of(2015, Month.MAY, 31, 0, 0), "Еда на граничное значение", 100); + public static final Meal MEAL5 = new Meal(MEAL1_ID + 4, of(2015, Month.MAY, 31, 10, 0), "Завтрак", 500); + public static final Meal MEAL6 = new Meal(MEAL1_ID + 5, of(2015, Month.MAY, 31, 13, 0), "Обед", 1000); + public static final Meal MEAL7 = new Meal(MEAL1_ID + 6, 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 = Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); + public static final List MEALS = Arrays.asList(MEAL7, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); public static Meal getCreated() { return new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Созданный ужин", 300); diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index f8df320023dc..ec4cbd2d25e0 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -3,6 +3,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.Util; @@ -22,6 +24,12 @@ public class InMemoryMealRepository implements MealRepository { // Map userId -> mealRepository private Map> usersMealsMap = new ConcurrentHashMap<>(); + { + InMemoryBaseRepository userMeals = new InMemoryBaseRepository<>(); + usersMealsMap.put(UserTestData.USER_ID, userMeals); + MealTestData.MEALS.forEach(meal -> userMeals.map.put(meal.getId(), meal)); + } + @Override public Meal save(Meal meal, int userId) { Objects.requireNonNull(meal, "meal must not be null"); diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index 46f9288d5238..e9fe11aa5019 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -28,10 +28,10 @@ public class MealServiceTest { @Autowired private MealService service; - @Test + @Test(expected = NotFoundException.class) public void delete() throws Exception { service.delete(MEAL1_ID, USER_ID); - assertMatch(service.getAll(USER_ID), MEAL6, MEAL5, MEAL4, MEAL3, MEAL2); + service.get(MEAL1_ID, USER_ID); } @Test(expected = NotFoundException.class) @@ -48,9 +48,10 @@ public void deleteNotOwn() throws Exception { public 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); + Integer newId = created.getId(); + newMeal.setId(newId); + assertMatch(created, newMeal); + assertMatch(service.get(newId, USER_ID), newMeal); } @Test @@ -92,4 +93,9 @@ public void getBetween() throws Exception { LocalDate.of(2015, Month.MAY, 30), LocalDate.of(2015, Month.MAY, 30), USER_ID), MEAL3, MEAL2, MEAL1); } + + @Test + public void getBetweenWithNullDates() throws Exception { + assertMatch(service.getBetweenDates(null, null, USER_ID), MEALS); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index dc9ad49482f3..ef91458a735e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -33,8 +33,10 @@ public class UserServiceTest { public 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(newUser); - newUser.setId(created.getId()); - assertMatch(service.getAll(), ADMIN, newUser, USER); + Integer newId = created.getId(); + newUser.setId(newId); + assertMatch(created, newUser); + assertMatch(service.get(newId), newUser); } @Test(expected = DataAccessException.class) @@ -42,10 +44,10 @@ public void duplicateMailCreate() throws Exception { service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.ROLE_USER)); } - @Test + @Test(expected = NotFoundException.class) public void delete() throws Exception { service.delete(USER_ID); - assertMatch(service.getAll(), ADMIN); + service.get(USER_ID); } @Test(expected = NotFoundException.class) From c03bf2edc68f4916f0352816bdceee4210c145ee Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:25:28 +0300 Subject: [PATCH 57/97] Applied patch - 5_0_2_change_between --- .../ru/javawebinar/topjava/repository/MealRepository.java | 5 +++-- .../topjava/repository/jdbc/JdbcMealRepository.java | 8 +++++--- .../topjava/repository/jpa/JpaMealRepository.java | 4 ++-- .../java/ru/javawebinar/topjava/service/MealService.java | 7 +------ .../java/ru/javawebinar/topjava/util/DateTimeUtil.java | 4 ---- .../repository/inmemory/InMemoryMealRepository.java | 8 +++----- 6 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java index f0578ff40457..3c7950b3a674 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -1,8 +1,9 @@ package ru.javawebinar.topjava.repository; +import org.springframework.lang.Nullable; import ru.javawebinar.topjava.model.Meal; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.List; public interface MealRepository { @@ -19,5 +20,5 @@ public interface MealRepository { List getAll(int userId); // ORDERED dateTime desc - List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId); + List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index fec32a8cca5b..3b052fdbb7e6 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -12,7 +12,7 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.List; @Repository @@ -79,9 +79,11 @@ public List getAll(int userId) { } @Override - public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { + startDate = (startDate == null ? LocalDate.MIN : startDate); + endDate = (endDate == null ? LocalDate.MAX : endDate); return jdbcTemplate.query( - "SELECT * FROM meals WHERE user_id=? AND date_time BETWEEN ? AND ? ORDER BY date_time DESC", + "SELECT * FROM meals WHERE user_id=? AND CAST(date_time AS DATE) BETWEEN ? AND ? ORDER BY date_time DESC", ROW_MAPPER, userId, startDate, endDate); } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java index e6fb632419b7..cef6590a3c8b 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -4,7 +4,7 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.List; @Repository @@ -31,7 +31,7 @@ public List getAll(int userId) { } @Override - public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { return null; } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 7d5c9653be1a..483118125769 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -5,10 +5,8 @@ import org.springframework.util.Assert; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; -import ru.javawebinar.topjava.util.DateTimeUtil; import java.time.LocalDate; -import java.time.LocalTime; import java.util.List; import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; @@ -31,10 +29,7 @@ public void delete(int id, int userId) { } public List getBetweenDates(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { - return repository.getBetween( - DateTimeUtil.createDateTime(startDate, LocalDate.MIN, LocalTime.MIN), - DateTimeUtil.createDateTime(endDate, LocalDate.MAX, LocalTime.MAX), - userId); + return repository.getBetweenInclusive(startDate, endDate, userId); } public List getAll(int userId) { diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index e8e0c323385b..f2f58b600d94 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -25,9 +25,5 @@ public static String toString(LocalDateTime ldt) { public static @Nullable LocalTime parseLocalTime(@Nullable String str) { return StringUtils.isEmpty(str) ? null : LocalTime.parse(str); } - - public static LocalDateTime createDateTime(@Nullable LocalDate date, LocalDate defaultDate, LocalTime time) { - return LocalDateTime.of(date != null ? date : defaultDate, time); - } } diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index ec4cbd2d25e0..091ab1f9bda7 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -11,7 +11,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; @@ -65,10 +65,8 @@ public List getAll(int userId) { } @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.isBetweenInclusive(meal.getDateTime(), startDateTime, endDateTime)); + public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { + return getAllFiltered(userId, meal -> Util.isBetweenInclusive(meal.getDate(), startDate, endDate)); } private List getAllFiltered(int userId, Predicate filter) { From 81e6cbc086c41cfd924a795c9d75ccf7a5bbc99a Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:44:26 +0300 Subject: [PATCH 58/97] Applied patch - 5_1_jdk_11 --- .travis.yml | 2 +- pom.xml | 8 +++++++- src/test/java/ru/javawebinar/topjava/MealTestData.java | 5 ++--- src/test/java/ru/javawebinar/topjava/UserTestData.java | 4 ++-- .../repository/inmemory/InMemoryMealRepository.java | 10 +++++----- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1cc16498abdd..e8509a60fea0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ # https://docs.travis-ci.com/user/languages/java/ language: java -jdk: openjdk8 +jdk: openjdk11 #https://dzone.com/articles/travis-ci-tutorial-java-projects cache: diff --git a/pom.xml b/pom.xml index c0c23925afe1..8929bc0786c7 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ http://topjava.herokuapp.com/ - 1.8 + 11 UTF-8 UTF-8 @@ -80,6 +80,12 @@ runtime + + javax.annotation + javax.annotation-api + 1.3.2 + + org.springframework diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index 61daeaaf34e9..cf6c2ae4e2c5 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -3,7 +3,6 @@ import ru.javawebinar.topjava.model.Meal; import java.time.Month; -import java.util.Arrays; import java.util.List; import static java.time.LocalDateTime.of; @@ -24,7 +23,7 @@ public class MealTestData { 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 = Arrays.asList(MEAL7, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); + public static final List MEALS = List.of(MEAL7, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); public static Meal getCreated() { return new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Созданный ужин", 300); @@ -39,7 +38,7 @@ public static void assertMatch(Meal actual, Meal expected) { } public static void assertMatch(Iterable actual, Meal... expected) { - assertMatch(actual, Arrays.asList(expected)); + assertMatch(actual, List.of(expected)); } public static void assertMatch(Iterable actual, Iterable expected) { diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index 3cc466c53754..ddb04d4df603 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -3,7 +3,7 @@ import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; -import java.util.Arrays; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; @@ -20,7 +20,7 @@ public static void assertMatch(User actual, User expected) { } public static void assertMatch(Iterable actual, User... expected) { - assertMatch(actual, Arrays.asList(expected)); + assertMatch(actual, List.of(expected)); } public static void assertMatch(Iterable actual, Iterable expected) { diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 091ab1f9bda7..236494ef4f33 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -25,7 +25,7 @@ public class InMemoryMealRepository implements MealRepository { private Map> usersMealsMap = new ConcurrentHashMap<>(); { - InMemoryBaseRepository userMeals = new InMemoryBaseRepository<>(); + var userMeals = new InMemoryBaseRepository(); usersMealsMap.put(UserTestData.USER_ID, userMeals); MealTestData.MEALS.forEach(meal -> userMeals.map.put(meal.getId(), meal)); } @@ -33,7 +33,7 @@ public class InMemoryMealRepository implements MealRepository { @Override public Meal save(Meal meal, int userId) { Objects.requireNonNull(meal, "meal must not be null"); - InMemoryBaseRepository meals = usersMealsMap.computeIfAbsent(userId, uid -> new InMemoryBaseRepository<>()); + var meals = usersMealsMap.computeIfAbsent(userId, uid -> new InMemoryBaseRepository<>()); return meals.save(meal); } @@ -49,13 +49,13 @@ public void preDestroy() { @Override public boolean delete(int id, int userId) { - InMemoryBaseRepository meals = usersMealsMap.get(userId); + var meals = usersMealsMap.get(userId); return meals != null && meals.delete(id); } @Override public Meal get(int id, int userId) { - InMemoryBaseRepository meals = usersMealsMap.get(userId); + var meals = usersMealsMap.get(userId); return meals == null ? null : meals.get(id); } @@ -70,7 +70,7 @@ public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, in } private List getAllFiltered(int userId, Predicate filter) { - InMemoryBaseRepository meals = usersMealsMap.get(userId); + var meals = usersMealsMap.get(userId); return meals == null ? Collections.emptyList() : meals.getCollection().stream() .filter(filter) From 149f518c5178c251a3b78e4a57d1b9e8b2451021 Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:44:58 +0300 Subject: [PATCH 59/97] Applied patch - 5_2_HW4 --- .../ru/javawebinar/topjava/model/Meal.java | 31 +++++++++++++- .../repository/jdbc/JdbcMealRepository.java | 9 ++-- .../repository/jpa/JpaMealRepository.java | 42 ++++++++++++++++--- .../topjava/util/DateTimeUtil.java | 17 ++++++++ .../ru/javawebinar/topjava/MealTestData.java | 4 +- 5 files changed, 90 insertions(+), 13 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 788ae8f6fe5b..a87fca59ac96 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,19 +1,46 @@ package ru.javawebinar.topjava.model; -import javax.persistence.FetchType; -import javax.persistence.ManyToOne; +import org.hibernate.validator.constraints.Range; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +@NamedQueries({ + @NamedQuery(name = Meal.ALL_SORTED, query = "SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC"), + @NamedQuery(name = Meal.DELETE, query = "DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId"), + @NamedQuery(name = Meal.GET_BETWEEN, query = "SELECT m FROM Meal m " + + "WHERE m.user.id=:userId AND m.dateTime >= :startDate AND m.dateTime < :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 private LocalDateTime dateTime; + @Column(name = "description", nullable = false) + @NotBlank + @Size(min = 2, max = 120) private String description; + @Column(name = "calories", nullable = false) + @Range(min = 10, max = 5000) private int calories; @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @NotNull private User user; public Meal() { diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index 3b052fdbb7e6..8f75082a482a 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -15,6 +15,9 @@ import java.time.LocalDate; import java.util.List; +import static ru.javawebinar.topjava.util.DateTimeUtil.getEndExclusive; +import static ru.javawebinar.topjava.util.DateTimeUtil.getStartInclusive; + @Repository public class JdbcMealRepository implements MealRepository { @@ -80,10 +83,8 @@ public List getAll(int userId) { @Override public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { - startDate = (startDate == null ? LocalDate.MIN : startDate); - endDate = (endDate == null ? LocalDate.MAX : endDate); return jdbcTemplate.query( - "SELECT * FROM meals WHERE user_id=? AND CAST(date_time AS DATE) BETWEEN ? AND ? ORDER BY date_time DESC", - ROW_MAPPER, userId, startDate, endDate); + "SELECT * FROM meals WHERE user_id=? AND date_time >=? AND date_time < ? ORDER BY date_time DESC", + ROW_MAPPER, userId, getStartInclusive(startDate), getEndExclusive(endDate)); } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java index cef6590a3c8b..d045860a4a90 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -1,37 +1,69 @@ 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.LocalDate; import java.util.List; +import static ru.javawebinar.topjava.util.DateTimeUtil.getEndExclusive; +import static ru.javawebinar.topjava.util.DateTimeUtil.getStartInclusive; + @Repository +@Transactional(readOnly = true) public class JpaMealRepository implements MealRepository { + @PersistenceContext + private EntityManager em; + @Override + @Transactional public Meal save(Meal meal, int userId) { - return null; + 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 false; + return em.createNamedQuery(Meal.DELETE) + .setParameter("id", id) + .setParameter("userId", userId) + .executeUpdate() != 0; } @Override public Meal get(int id, int userId) { - return null; + Meal meal = em.find(Meal.class, id); + return meal != null && meal.getUser().getId() == userId ? meal : null; } @Override public List getAll(int userId) { - return null; + return em.createNamedQuery(Meal.ALL_SORTED, Meal.class) + .setParameter("userId", userId) + .getResultList(); } @Override public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { - return null; + return em.createNamedQuery(Meal.GET_BETWEEN, Meal.class) + .setParameter("userId", userId) + .setParameter("startDate", getStartInclusive(startDate)) + .setParameter("endDate", getEndExclusive(endDate)) + .getResultList(); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index f2f58b600d94..09cb846b390f 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -7,10 +7,15 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; public class DateTimeUtil { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + // HSQLDB 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() { } @@ -25,5 +30,17 @@ public static String toString(LocalDateTime ldt) { public static @Nullable LocalTime parseLocalTime(@Nullable String str) { return StringUtils.isEmpty(str) ? null : LocalTime.parse(str); } + + public static LocalDateTime getStartInclusive(LocalDate localDate) { + return startOfDay(localDate != null ? localDate : MIN_DATE); + } + + public static LocalDateTime getEndExclusive(LocalDate localDate) { + return startOfDay(localDate != null ? localDate.plus(1, ChronoUnit.DAYS) : MAX_DATE); + } + + private static LocalDateTime startOfDay(LocalDate localDate) { + return LocalDateTime.of(localDate, LocalTime.MIN); + } } diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index cf6c2ae4e2c5..5e4b82dfda64 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -34,7 +34,7 @@ public static Meal getUpdated() { } public static void assertMatch(Meal actual, Meal expected) { - assertThat(actual).isEqualToComparingFieldByField(expected); + assertThat(actual).isEqualToIgnoringGivenFields(expected, "user"); } public static void assertMatch(Iterable actual, Meal... expected) { @@ -42,6 +42,6 @@ public static void assertMatch(Iterable actual, Meal... expected) { } public static void assertMatch(Iterable actual, Iterable expected) { - assertThat(actual).usingFieldByFieldElementComparator().isEqualTo(expected); + assertThat(actual).usingElementComparatorIgnoringFields("user").isEqualTo(expected); } } From 7283b1860407a64c6864246b42a63c7dec365649 Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:45:17 +0300 Subject: [PATCH 60/97] Applied patch - 5_3_fix_hibernate_issue --- .../ru/javawebinar/topjava/model/AbstractBaseEntity.java | 7 ++++++- src/main/resources/spring/spring-db.xml | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index acee5c76ee5d..cf9d139e5a75 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.model; +import org.hibernate.Hibernate; + import javax.persistence.*; @MappedSuperclass @@ -11,6 +13,9 @@ public abstract class AbstractBaseEntity { @Id @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") + +// See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034 +// Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting protected Integer id; protected AbstractBaseEntity() { @@ -42,7 +47,7 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (o == null || !getClass().equals(Hibernate.getClass(o))) { return false; } AbstractBaseEntity that = (AbstractBaseEntity) o; diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 71a6b17771f6..9fed56d2c8b8 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -48,6 +48,7 @@ + From 84056393dcee6a1325ed7e3dcbd458ead9531d7d Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:45:47 +0300 Subject: [PATCH 61/97] Applied patch - 5_4_HW4_optional --- .../topjava/service/MealServiceTest.java | 59 ++++++++++++++++--- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index e9fe11aa5019..ec29174d74eb 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -1,18 +1,26 @@ package ru.javawebinar.topjava.service; +import org.junit.AfterClass; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.Stopwatch; +import org.junit.runner.Description; import org.junit.runner.RunWith; +import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringRunner; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.util.exception.NotFoundException; import java.time.LocalDate; import java.time.Month; +import java.util.concurrent.TimeUnit; +import static org.slf4j.LoggerFactory.getLogger; import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; import static ru.javawebinar.topjava.UserTestData.USER_ID; @@ -21,26 +29,55 @@ "classpath:spring/spring-app.xml", "classpath:spring/spring-db.xml" }) -@RunWith(SpringJUnit4ClassRunner.class) +@RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) public class MealServiceTest { + private static final Logger log = getLogger(MealServiceTest.class); + + private static StringBuilder results = new StringBuilder(); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev + public Stopwatch stopwatch = new Stopwatch() { + @Override + protected void finished(long nanos, Description description) { + String result = String.format("\n%-25s %7d", description.getMethodName(), TimeUnit.NANOSECONDS.toMillis(nanos)); + results.append(result); + log.info(result + " ms\n"); + } + }; + + @AfterClass + public static void printResult() { + log.info("\n---------------------------------" + + "\nTest Duration, ms" + + "\n---------------------------------" + + results + + "\n---------------------------------"); + } @Autowired private MealService service; - @Test(expected = NotFoundException.class) + @Test public void delete() throws Exception { service.delete(MEAL1_ID, USER_ID); + thrown.expect(NotFoundException.class); service.get(MEAL1_ID, USER_ID); } - @Test(expected = NotFoundException.class) + @Test public void deleteNotFound() throws Exception { + thrown.expect(NotFoundException.class); service.delete(1, USER_ID); } - @Test(expected = NotFoundException.class) + @Test public void deleteNotOwn() throws Exception { + thrown.expect(NotFoundException.class); service.delete(MEAL1_ID, ADMIN_ID); } @@ -60,13 +97,15 @@ public void get() throws Exception { assertMatch(actual, ADMIN_MEAL1); } - @Test(expected = NotFoundException.class) + @Test public void getNotFound() throws Exception { - service.get(1, USER_ID); + thrown.expect(NotFoundException.class); + service.get(MEAL1_ID, ADMIN_ID); } - @Test(expected = NotFoundException.class) + @Test public void getNotOwn() throws Exception { + thrown.expect(NotFoundException.class); service.get(MEAL1_ID, ADMIN_ID); } @@ -77,8 +116,10 @@ public void update() throws Exception { assertMatch(service.get(MEAL1_ID, USER_ID), updated); } - @Test(expected = NotFoundException.class) + @Test public void updateNotFound() throws Exception { + thrown.expect(NotFoundException.class); + thrown.expectMessage("Not found entity with id=" + MEAL1_ID); service.update(MEAL1, ADMIN_ID); } From 2257047dcc9bc220428bacff69832ab71280c7bc Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:47:41 +0300 Subject: [PATCH 62/97] Applied patch - 5_5_log_colored --- src/main/resources/logback.xml | 2 +- .../topjava/service/MealServiceTest.java | 2 +- src/test/resources/logback-test.xml | 17 ++++++++++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index c7bffc3a958c..12a4f63c6c15 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -16,7 +16,7 @@ UTF-8 - %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index ec29174d74eb..0064b4fd6b38 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -32,7 +32,7 @@ @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) public class MealServiceTest { - private static final Logger log = getLogger(MealServiceTest.class); + private static final Logger log = getLogger("result"); private static StringBuilder results = new StringBuilder(); diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 428ceca2ee02..019d8a1edcb6 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -7,12 +7,23 @@ UTF-8 - %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n - - + + + UTF-8 + %magenta(%msg%n) + + + + + + + + + From c4ff8e40e89f8ec413ac07222d3784da3115e5bd Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:51:04 +0300 Subject: [PATCH 63/97] Applied patch - 5_6_profiles_connection_pool --- pom.xml | 57 ++++++++++++------- .../java/ru/javawebinar/topjava/Profiles.java | 15 +++++ src/main/resources/db/hsqldb.properties | 1 - src/main/resources/db/postgres.properties | 1 - src/main/resources/spring/spring-db.xml | 52 ++++++++++------- .../topjava/service/MealServiceTest.java | 3 + .../topjava/service/UserServiceTest.java | 3 + 7 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/Profiles.java diff --git a/pom.xml b/pom.xml index 8929bc0786c7..8470d2a8bf0c 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ UTF-8 5.2.0.RELEASE + 9.0.27 1.2.3 @@ -66,13 +67,6 @@ compile - - org.slf4j - jul-to-slf4j - ${slf4j.version} - runtime - - ch.qos.logback logback-classic @@ -98,19 +92,6 @@ ${spring.version} - - - org.postgresql - postgresql - ${postgresql.version} - - - org.hsqldb - hsqldb - 2.3.4 - - - org.hibernate @@ -167,6 +148,42 @@ + + hsqldb + + + org.hsqldb + hsqldb + 2.3.4 + + + + + + postgres + + + org.postgresql + postgresql + ${postgresql.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat.version} + provided + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + runtime + + + + true + + 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..c438cbbe7a6e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -0,0 +1,15 @@ +package ru.javawebinar.topjava; + +public class Profiles { + public static final String + JDBC = "jdbc", + JPA = "jpa"; + + public static final String REPOSITORY_IMPLEMENTATION = JPA; + + public static final String + POSTGRES_DB = "postgres", + HSQL_DB = "hsqldb"; + + public static final String ACTIVE_DB = POSTGRES_DB; +} diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties index 6371e7b8073e..0ea68eb3d308 100644 --- a/src/main/resources/db/hsqldb.properties +++ b/src/main/resources/db/hsqldb.properties @@ -3,7 +3,6 @@ database.url=jdbc:hsqldb:mem:topjava database.username=sa database.password= -database.driverClassName=org.hsqldb.jdbcDriver database.init=true jdbc.initLocation=initDB_hsql.sql diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index f9a174a19897..cc20ca447a7a 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -5,7 +5,6 @@ database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password -database.driverClassName=org.postgresql.Driver database.init=true jdbc.initLocation=initDB.sql diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 9fed56d2c8b8..374f5ec27eca 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -12,16 +12,7 @@ - - - - - - + @@ -30,15 +21,6 @@ - - - - - - - - @@ -76,4 +58,36 @@ --> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index 0064b4fd6b38..3aafb52781bd 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -9,10 +9,12 @@ import org.junit.runner.RunWith; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.context.junit4.SpringRunner; +import ru.javawebinar.topjava.Profiles; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -31,6 +33,7 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ActiveProfiles(Profiles.ACTIVE_DB) public class MealServiceTest { private static final Logger log = getLogger("result"); diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index ef91458a735e..23fb2149744c 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -4,10 +4,12 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.context.junit4.SpringRunner; +import ru.javawebinar.topjava.Profiles; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -24,6 +26,7 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ActiveProfiles(Profiles.ACTIVE_DB) public class UserServiceTest { @Autowired From 4f2193bca639f9d292103f3724c189a9d4b52cc9 Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:52:15 +0300 Subject: [PATCH 64/97] Applied patch - 5_7_profile_resolver --- .../java/ru/javawebinar/topjava/Profiles.java | 15 ++++++++++++++- .../topjava/ActiveDbProfileResolver.java | 12 ++++++++++++ .../topjava/service/MealServiceTest.java | 4 ++-- .../topjava/service/UserServiceTest.java | 4 ++-- 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java index c438cbbe7a6e..a11b37a256dd 100644 --- a/src/main/java/ru/javawebinar/topjava/Profiles.java +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -11,5 +11,18 @@ public class Profiles { POSTGRES_DB = "postgres", HSQL_DB = "hsqldb"; - public static final String ACTIVE_DB = POSTGRES_DB; + // Get DB profile depending of DB driver in classpath + public static String getActiveDbProfile() { + 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/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/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index 3aafb52781bd..b0c48dbd6445 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -14,7 +14,7 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.context.junit4.SpringRunner; -import ru.javawebinar.topjava.Profiles; +import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -33,7 +33,7 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) -@ActiveProfiles(Profiles.ACTIVE_DB) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) public class MealServiceTest { private static final Logger log = getLogger("result"); diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index 23fb2149744c..6aa7fc214a4f 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -9,7 +9,7 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.context.junit4.SpringRunner; -import ru.javawebinar.topjava.Profiles; +import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -26,7 +26,7 @@ }) @RunWith(SpringRunner.class) @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) -@ActiveProfiles(Profiles.ACTIVE_DB) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) public class UserServiceTest { @Autowired From fb4abf00b3a420cb1909a8e8e5466ceec3fdc527 Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:53:02 +0300 Subject: [PATCH 65/97] Applied patch - 5_8_spring_data_jpa --- pom.xml | 18 +++++--- .../java/ru/javawebinar/topjava/Profiles.java | 5 ++- .../topjava/model/AbstractBaseEntity.java | 5 ++- .../datajpa/CrudMealRepository.java | 7 ++++ .../datajpa/CrudUserRepository.java | 19 +++++++++ .../datajpa/DataJpaMealRepository.java | 41 ++++++++++++++++++ .../datajpa/DataJpaUserRepository.java | 42 +++++++++++++++++++ src/main/resources/spring/spring-db.xml | 16 ++++++- 8 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java diff --git a/pom.xml b/pom.xml index 8470d2a8bf0c..2cdde8048de2 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ UTF-8 5.2.0.RELEASE + 2.2.0.RELEASE 9.0.27 @@ -84,12 +85,11 @@ org.springframework spring-context - ${spring.version} - org.springframework - spring-orm - ${spring.version} + org.springframework.data + spring-data-jpa + ${spring-data-jpa.version} @@ -136,7 +136,6 @@ org.springframework spring-test - ${spring.version} test @@ -187,5 +186,14 @@ + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java index a11b37a256dd..d6ef2e0379ed 100644 --- a/src/main/java/ru/javawebinar/topjava/Profiles.java +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -3,9 +3,10 @@ public class Profiles { public static final String JDBC = "jdbc", - JPA = "jpa"; + JPA = "jpa", + DATAJPA = "datajpa"; - public static final String REPOSITORY_IMPLEMENTATION = JPA; + public static final String REPOSITORY_IMPLEMENTATION = DATAJPA; public static final String POSTGRES_DB = "postgres", diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index cf9d139e5a75..fefa8d7e6acf 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,13 +1,14 @@ package ru.javawebinar.topjava.model; import org.hibernate.Hibernate; +import org.springframework.data.domain.Persistable; import javax.persistence.*; @MappedSuperclass // http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access @Access(AccessType.FIELD) -public abstract class AbstractBaseEntity { +public abstract class AbstractBaseEntity implements Persistable { public static final int START_SEQ = 100000; @Id @@ -29,10 +30,12 @@ public void setId(Integer id) { this.id = id; } + @Override public Integer getId() { return id; } + @Override public boolean isNew() { return this.id == null; } 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..a3659675c910 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.javawebinar.topjava.model.Meal; + +public interface CrudMealRepository extends JpaRepository { +} 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..24c42a814220 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -0,0 +1,19 @@ +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.User; + +@Transactional(readOnly = true) +public interface CrudUserRepository extends JpaRepository { + @Transactional + @Modifying +// @Query(name = User.DELETE) + @Query("DELETE FROM User u WHERE u.id=:id") + int delete(@Param("id") int id); + + User getByEmail(String email); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java new file mode 100644 index 000000000000..a4b19cc491d9 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java @@ -0,0 +1,41 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDate; +import java.util.List; + +@Repository +public class DataJpaMealRepository implements MealRepository { + + @Autowired + private CrudMealRepository crudRepository; + + @Override + public Meal save(Meal meal, int userId) { + return null; + } + + @Override + public boolean delete(int id, int userId) { + return false; + } + + @Override + public Meal get(int id, int userId) { + return null; + } + + @Override + public List getAll(int userId) { + return null; + } + + @Override + public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { + return null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java new file mode 100644 index 000000000000..55ebf8a1098a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +@Repository +public class DataJpaUserRepository implements UserRepository { + private static final Sort SORT_NAME_EMAIL = Sort.by(Sort.Direction.ASC, "name", "email"); + + @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); + } +} diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 374f5ec27eca..3cfe933f3796 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -4,17 +4,22 @@ xmlns:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" + xmlns:jpa="http://www.springframework.org/schema/data/jpa" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd - http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> + http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> - + + + + @@ -90,4 +95,11 @@ p:username="${database.username}" p:password="${database.password}"/> + + + + + + + \ No newline at end of file From 22df5a354159c5e6121c60cb8e695a5353504009 Mon Sep 17 00:00:00 2001 From: alkone Date: Tue, 5 Nov 2019 14:53:34 +0300 Subject: [PATCH 66/97] Applied patch - 5_9_spring_cache --- pom.xml | 25 ++++++++++++++++++- .../topjava/service/UserService.java | 6 +++++ src/main/resources/cache/ehcache.xml | 25 +++++++++++++++++++ src/main/resources/spring/spring-app.xml | 1 + src/main/resources/spring/spring-tools.xml | 22 ++++++++++++++++ .../topjava/service/UserServiceTest.java | 10 ++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/cache/ehcache.xml create mode 100644 src/main/resources/spring/spring-tools.xml diff --git a/pom.xml b/pom.xml index 2cdde8048de2..e23c6fc0642a 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,9 @@ 5.4.7.Final 6.0.17.Final 3.0.1-b11 + + + 3.8.1 @@ -84,7 +87,7 @@ org.springframework - spring-context + spring-context-support org.springframework.data @@ -112,6 +115,26 @@ provided + + + javax.cache + cache-api + 1.1.0 + + + org.ehcache + ehcache + + ${ehcache.version} + + + + + org.glassfish.jaxb + jaxb-runtime + 2.4.0-b180830.0438 + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index d9811b3abda7..08f7f0e0b5f1 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -1,6 +1,8 @@ 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.stereotype.Service; import org.springframework.util.Assert; import ru.javawebinar.topjava.model.User; @@ -22,11 +24,13 @@ public UserService(UserRepository repository) { this.repository = repository; } + @CacheEvict(value = "users", allEntries = true) public User create(User user) { Assert.notNull(user, "user must not be null"); return repository.save(user); } + @CacheEvict(value = "users", allEntries = true) public void delete(int id) throws NotFoundException { checkNotFoundWithId(repository.delete(id), id); } @@ -40,10 +44,12 @@ public User getByEmail(String email) throws NotFoundException { return checkNotFound(repository.getByEmail(email), "email=" + email); } + @Cacheable("users") public List getAll() { return repository.getAll(); } + @CacheEvict(value = "users", allEntries = true) public void update(User user) throws NotFoundException { Assert.notNull(user, "user must not be null"); checkNotFoundWithId(repository.save(user), user.getId()); 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/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index b220a8ba3bf4..8aa867cc15e2 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -11,6 +11,7 @@ --> + 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/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index 6aa7fc214a4f..dbae67277c3b 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -1,8 +1,10 @@ package ru.javawebinar.topjava.service; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; import org.springframework.dao.DataAccessException; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -32,6 +34,14 @@ public class UserServiceTest { @Autowired private UserService service; + @Autowired + private CacheManager cacheManager; + + @Before + public void setUp() throws Exception { + cacheManager.getCache("users").clear(); + } + @Test public void create() throws Exception { User newUser = new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.ROLE_USER)); From 259cb70c631f83ccc915b0bda915fe043bf39822 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:45:21 +0300 Subject: [PATCH 67/97] Applied patch - 6_0_fix --- .../topjava/repository/jpa/JpaMealRepository.java | 8 +++----- .../ru/javawebinar/topjava/service/UserService.java | 9 ++++----- .../ru/javawebinar/topjava/util/DateTimeUtil.java | 8 ++------ .../java/ru/javawebinar/topjava/MealTestData.java | 2 +- .../java/ru/javawebinar/topjava/UserTestData.java | 13 +++++++++++++ .../topjava/service/MealServiceTest.java | 10 +++++----- .../topjava/service/UserServiceTest.java | 8 ++------ 7 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java index d045860a4a90..89e02e45ec46 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -24,16 +24,14 @@ public class JpaMealRepository implements MealRepository { @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); + } else if (get(meal.getId(), userId) == null) { + return null; } + return em.merge(meal); } @Override diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 08f7f0e0b5f1..1405b4757ea7 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -7,7 +7,6 @@ import org.springframework.util.Assert; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; -import ru.javawebinar.topjava.util.exception.NotFoundException; import java.util.List; @@ -31,15 +30,15 @@ public User create(User user) { } @CacheEvict(value = "users", allEntries = true) - public void delete(int id) throws NotFoundException { + public void delete(int id) { checkNotFoundWithId(repository.delete(id), id); } - public User get(int id) throws NotFoundException { + public User get(int id) { return checkNotFoundWithId(repository.get(id), id); } - public User getByEmail(String email) throws NotFoundException { + public User getByEmail(String email) { Assert.notNull(email, "email must not be null"); return checkNotFound(repository.getByEmail(email), "email=" + email); } @@ -50,7 +49,7 @@ public List getAll() { } @CacheEvict(value = "users", allEntries = true) - public void update(User user) throws NotFoundException { + public void update(User user) { Assert.notNull(user, "user must not be null"); checkNotFoundWithId(repository.save(user), user.getId()); } diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 09cb846b390f..61ebc14682e4 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -32,15 +32,11 @@ public static String toString(LocalDateTime ldt) { } public static LocalDateTime getStartInclusive(LocalDate localDate) { - return startOfDay(localDate != null ? localDate : MIN_DATE); + return (localDate != null ? localDate : MIN_DATE).atStartOfDay(); } public static LocalDateTime getEndExclusive(LocalDate localDate) { - return startOfDay(localDate != null ? localDate.plus(1, ChronoUnit.DAYS) : MAX_DATE); - } - - private static LocalDateTime startOfDay(LocalDate localDate) { - return LocalDateTime.of(localDate, LocalTime.MIN); + return (localDate != null ? localDate.plus(1, ChronoUnit.DAYS) : MAX_DATE).atStartOfDay(); } } diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index 5e4b82dfda64..c45097cea056 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -25,7 +25,7 @@ public class MealTestData { public static final List MEALS = List.of(MEAL7, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); - public static Meal getCreated() { + public static Meal getNew() { return new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Созданный ужин", 300); } diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index ddb04d4df603..bf27d417a1f1 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -3,6 +3,8 @@ import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; +import java.util.Collections; +import java.util.Date; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -15,6 +17,17 @@ public class UserTestData { public static final User USER = new User(USER_ID, "User", "user@yandex.ru", "password", Role.ROLE_USER); public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ROLE_ADMIN); + public static User getNew() { + return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.ROLE_USER)); + } + + public static User getUpdated() { + User updated = new User(USER); + updated.setName("UpdatedName"); + updated.setCaloriesPerDay(330); + return updated; + } + public static void assertMatch(User actual, User expected) { assertThat(actual).isEqualToIgnoringGivenFields(expected, "registered", "roles"); } diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java index b0c48dbd6445..b4bd13255a1a 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java @@ -39,6 +39,9 @@ public class MealServiceTest { private static StringBuilder results = new StringBuilder(); + @Autowired + private MealService service; + @Rule public ExpectedException thrown = ExpectedException.none(); @@ -62,9 +65,6 @@ public static void printResult() { "\n---------------------------------"); } - @Autowired - private MealService service; - @Test public void delete() throws Exception { service.delete(MEAL1_ID, USER_ID); @@ -86,7 +86,7 @@ public void deleteNotOwn() throws Exception { @Test public void create() throws Exception { - Meal newMeal = getCreated(); + Meal newMeal = getNew(); Meal created = service.create(newMeal, USER_ID); Integer newId = created.getId(); newMeal.setId(newId); @@ -103,7 +103,7 @@ public void get() throws Exception { @Test public void getNotFound() throws Exception { thrown.expect(NotFoundException.class); - service.get(MEAL1_ID, ADMIN_ID); + service.get(1, ADMIN_ID); } @Test diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java index dbae67277c3b..93b7efa30e41 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java @@ -16,8 +16,6 @@ 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 ru.javawebinar.topjava.UserTestData.*; @@ -44,7 +42,7 @@ public void setUp() throws Exception { @Test public void create() throws Exception { - User newUser = new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.ROLE_USER)); + User newUser = getNew(); User created = service.create(newUser); Integer newId = created.getId(); newUser.setId(newId); @@ -87,9 +85,7 @@ public void getByEmail() throws Exception { @Test public void update() throws Exception { - User updated = new User(USER); - updated.setName("UpdatedName"); - updated.setCaloriesPerDay(330); + User updated = getUpdated(); service.update(updated); assertMatch(service.get(USER_ID), updated); } From bebb11a58e339061392d18c0f5353c5b71b5d765 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:48:05 +0300 Subject: [PATCH 68/97] Applied patch - 6_01_HW5_data_jpa --- .../topjava/repository/MealRepository.java | 12 ++++++++- .../datajpa/CrudMealRepository.java | 25 ++++++++++++++++++- .../datajpa/DataJpaMealRepository.java | 25 +++++++++++++------ .../repository/jdbc/JdbcMealRepository.java | 9 +++---- .../repository/jpa/JpaMealRepository.java | 11 +++----- .../inmemory/InMemoryMealRepository.java | 6 ++--- 6 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java index 3c7950b3a674..c633552ff220 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -1,11 +1,16 @@ package ru.javawebinar.topjava.repository; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import ru.javawebinar.topjava.model.Meal; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; +import static ru.javawebinar.topjava.util.DateTimeUtil.getEndExclusive; +import static ru.javawebinar.topjava.util.DateTimeUtil.getStartInclusive; + public interface MealRepository { // null if updated meal do not belong to userId Meal save(Meal meal, int userId); @@ -20,5 +25,10 @@ public interface MealRepository { List getAll(int userId); // ORDERED dateTime desc - List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId); + default List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { + return getBetweenInclusive(getStartInclusive(startDate), getEndExclusive(endDate), userId); + } + + // ORDERED dateTime desc + List getBetweenInclusive(@NonNull LocalDateTime startDate, @NonNull LocalDateTime endDate, int userId); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java index a3659675c910..b04ad49bf463 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java @@ -1,7 +1,30 @@ 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); + + @Query("SELECT m from Meal m WHERE m.user.id=:userId AND m.dateTime >= :startDate AND m.dateTime < :endDate ORDER BY m.dateTime DESC") + List getBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("userId") int userId); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java index a4b19cc491d9..68280d50537b 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java @@ -2,40 +2,49 @@ 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.LocalDate; +import java.time.LocalDateTime; import java.util.List; @Repository public class DataJpaMealRepository implements MealRepository { @Autowired - private CrudMealRepository crudRepository; + private CrudMealRepository crudMealRepository; + + @Autowired + private CrudUserRepository crudUserRepository; @Override + @Transactional public Meal save(Meal meal, int userId) { - return null; + 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 false; + return crudMealRepository.delete(id, userId) != 0; } @Override public Meal get(int id, int userId) { - return null; + return crudMealRepository.findById(id).filter(meal -> meal.getUser().getId() == userId).orElse(null); } @Override public List getAll(int userId) { - return null; + return crudMealRepository.getAll(userId); } @Override - public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { - return null; + public List getBetweenInclusive(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return crudMealRepository.getBetween(startDateTime, endDateTime, userId); } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index 8f75082a482a..eafab7809baa 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -12,12 +12,9 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; -import static ru.javawebinar.topjava.util.DateTimeUtil.getEndExclusive; -import static ru.javawebinar.topjava.util.DateTimeUtil.getStartInclusive; - @Repository public class JdbcMealRepository implements MealRepository { @@ -82,9 +79,9 @@ public List getAll(int userId) { } @Override - public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { + public List getBetweenInclusive(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return jdbcTemplate.query( "SELECT * FROM meals WHERE user_id=? AND date_time >=? AND date_time < ? ORDER BY date_time DESC", - ROW_MAPPER, userId, getStartInclusive(startDate), getEndExclusive(endDate)); + ROW_MAPPER, userId, startDateTime, endDateTime); } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java index 89e02e45ec46..b1fa133bc6e0 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -8,12 +8,9 @@ import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; -import static ru.javawebinar.topjava.util.DateTimeUtil.getEndExclusive; -import static ru.javawebinar.topjava.util.DateTimeUtil.getStartInclusive; - @Repository @Transactional(readOnly = true) public class JpaMealRepository implements MealRepository { @@ -57,11 +54,11 @@ public List getAll(int userId) { } @Override - public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { + public List getBetweenInclusive(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return em.createNamedQuery(Meal.GET_BETWEEN, Meal.class) .setParameter("userId", userId) - .setParameter("startDate", getStartInclusive(startDate)) - .setParameter("endDate", getEndExclusive(endDate)) + .setParameter("startDate", startDateTime) + .setParameter("endDate", endDateTime) .getResultList(); } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java index 236494ef4f33..93f9b9295630 100644 --- a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -11,7 +11,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; @@ -65,8 +65,8 @@ public List getAll(int userId) { } @Override - public List getBetweenInclusive(LocalDate startDate, LocalDate endDate, int userId) { - return getAllFiltered(userId, meal -> Util.isBetweenInclusive(meal.getDate(), startDate, endDate)); + public List getBetweenInclusive(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return getAllFiltered(userId, meal -> Util.isBetweenInclusive(meal.getDateTime(), startDateTime, endDateTime)); } private List getAllFiltered(int userId, Predicate filter) { From 03f26d10f402e30be7cf08b87270043d1ed0c5fc Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:49:27 +0300 Subject: [PATCH 69/97] Applied patch - 6_02_HW5_profile_test --- src/main/resources/spring/spring-db.xml | 89 +++++++++---------- ...Test.java => AbstractMealServiceTest.java} | 50 +---------- .../topjava/service/AbstractServiceTest.java | 58 ++++++++++++ ...Test.java => AbstractUserServiceTest.java} | 16 +--- .../datajpa/DataJpaMealServiceTest.java | 10 +++ .../datajpa/DataJpaUserServiceTest.java | 10 +++ .../service/jdbc/JdbcMealServiceTest.java | 10 +++ .../service/jdbc/JdbcUserServiceTest.java | 10 +++ .../service/jpa/JpaMealServiceTest.java | 10 +++ .../service/jpa/JpaUserServiceTest.java | 10 +++ 10 files changed, 162 insertions(+), 111 deletions(-) rename src/test/java/ru/javawebinar/topjava/service/{MealServiceTest.java => AbstractMealServiceTest.java} (58%) create mode 100644 src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java rename src/test/java/ru/javawebinar/topjava/service/{UserServiceTest.java => AbstractUserServiceTest.java} (76%) create mode 100644 src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 3cfe933f3796..a64af1dd224d 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -11,58 +11,12 @@ http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -97,9 +51,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java similarity index 58% rename from src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java rename to src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index b4bd13255a1a..576da464b72c 100644 --- a/src/test/java/ru/javawebinar/topjava/service/MealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -1,70 +1,22 @@ package ru.javawebinar.topjava.service; -import org.junit.AfterClass; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.Stopwatch; -import org.junit.runner.Description; -import org.junit.runner.RunWith; -import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.SqlConfig; -import org.springframework.test.context.junit4.SpringRunner; -import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.util.exception.NotFoundException; import java.time.LocalDate; import java.time.Month; -import java.util.concurrent.TimeUnit; -import static org.slf4j.LoggerFactory.getLogger; import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; import static ru.javawebinar.topjava.UserTestData.USER_ID; -@ContextConfiguration({ - "classpath:spring/spring-app.xml", - "classpath:spring/spring-db.xml" -}) -@RunWith(SpringRunner.class) -@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) -@ActiveProfiles(resolver = ActiveDbProfileResolver.class) -public class MealServiceTest { - private static final Logger log = getLogger("result"); - - private static StringBuilder results = new StringBuilder(); +public abstract class AbstractMealServiceTest extends AbstractServiceTest { @Autowired private MealService service; - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Rule - // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev - public Stopwatch stopwatch = new Stopwatch() { - @Override - protected void finished(long nanos, Description description) { - String result = String.format("\n%-25s %7d", description.getMethodName(), TimeUnit.NANOSECONDS.toMillis(nanos)); - results.append(result); - log.info(result + " ms\n"); - } - }; - - @AfterClass - public static void printResult() { - log.info("\n---------------------------------" + - "\nTest Duration, ms" + - "\n---------------------------------" + - results + - "\n---------------------------------"); - } - @Test public void delete() throws Exception { service.delete(MEAL1_ID, USER_ID); 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..04ee8a9d2dcf --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -0,0 +1,58 @@ +package ru.javawebinar.topjava.service; + +import org.junit.AfterClass; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.rules.Stopwatch; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringRunner; +import ru.javawebinar.topjava.ActiveDbProfileResolver; + +import java.util.concurrent.TimeUnit; + +import static org.slf4j.LoggerFactory.getLogger; + +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-db.xml" +}) +@RunWith(SpringRunner.class) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) +abstract public class AbstractServiceTest { + private static final Logger log = getLogger("result"); + + private static StringBuilder results = new StringBuilder(); + + // https://dzone.com/articles/applying-new-jdk-11-string-methods + private static String DELIM = "-".repeat(103); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev + public Stopwatch stopwatch = new Stopwatch() { + @Override + protected void finished(long nanos, Description description) { + String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos)); + results.append(result).append('\n'); + log.info(result + " ms\n"); + } + }; + + + @AfterClass + public static void printResult() { + log.info("\n" + DELIM + + "\nTest Duration, ms" + + "\n" + DELIM + "\n" + results + DELIM + "\n"); + results.setLength(0); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java similarity index 76% rename from src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java rename to src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 93b7efa30e41..d250bef2bbac 100644 --- a/src/test/java/ru/javawebinar/topjava/service/UserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -2,16 +2,9 @@ import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; import org.springframework.dao.DataAccessException; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.context.jdbc.SqlConfig; -import org.springframework.test.context.junit4.SpringRunner; -import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -20,14 +13,7 @@ import static ru.javawebinar.topjava.UserTestData.*; -@ContextConfiguration({ - "classpath:spring/spring-app.xml", - "classpath:spring/spring-db.xml" -}) -@RunWith(SpringRunner.class) -@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) -@ActiveProfiles(resolver = ActiveDbProfileResolver.class) -public class UserServiceTest { +public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired private UserService service; 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..e28cc6d6bc53 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.DATAJPA; + +@ActiveProfiles(DATAJPA) +public class DataJpaMealServiceTest extends AbstractMealServiceTest { +} 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..a0757e739339 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.DATAJPA; + +@ActiveProfiles(DATAJPA) +public class DataJpaUserServiceTest extends AbstractUserServiceTest { +} \ 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..9ff4ae615b2b --- /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) +public 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..419f68ed1098 --- /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) +public 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..70e7bf865421 --- /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) +public 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..d1b3e4699785 --- /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.AbstractUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles(JPA) +public class JpaUserServiceTest extends AbstractUserServiceTest { +} \ No newline at end of file From 30ce62d1f080442d2705ee2a45614576f4ff1afe Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:50:07 +0300 Subject: [PATCH 70/97] Applied patch - 6_03_extract_rules --- .../ru/javawebinar/topjava/TimingRules.java | 42 +++++++++++++++++++ .../topjava/service/AbstractServiceTest.java | 39 ++++------------- 2 files changed, 49 insertions(+), 32 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/TimingRules.java diff --git a/src/test/java/ru/javawebinar/topjava/TimingRules.java b/src/test/java/ru/javawebinar/topjava/TimingRules.java new file mode 100644 index 000000000000..5a13a6f84fed --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TimingRules.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava; + +import org.junit.rules.ExternalResource; +import org.junit.rules.Stopwatch; +import org.junit.runner.Description; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class TimingRules { + private static final Logger log = LoggerFactory.getLogger("result"); + + private static StringBuilder results = new StringBuilder(); + + // https://dzone.com/articles/applying-new-jdk-11-string-methods + private static String DELIM = "-".repeat(103); + + // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev + public static final Stopwatch STOPWATCH = new Stopwatch() { + @Override + protected void finished(long nanos, Description description) { + String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos)); + results.append(result).append('\n'); + log.info(result + " ms\n"); + } + }; + + public static final ExternalResource SUMMARY = new ExternalResource() { + @Override + protected void before() throws Throwable { + results.setLength(0); + } + + @Override + protected void after() { + log.info("\n" + DELIM + + "\nTest Duration, ms" + + "\n" + DELIM + "\n" + results + DELIM + "\n"); + } + }; +} diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index 04ee8a9d2dcf..d7a270369caa 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -1,22 +1,18 @@ package ru.javawebinar.topjava.service; -import org.junit.AfterClass; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.rules.ExpectedException; +import org.junit.rules.ExternalResource; import org.junit.rules.Stopwatch; -import org.junit.runner.Description; import org.junit.runner.RunWith; -import org.slf4j.Logger; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.context.junit4.SpringRunner; import ru.javawebinar.topjava.ActiveDbProfileResolver; - -import java.util.concurrent.TimeUnit; - -import static org.slf4j.LoggerFactory.getLogger; +import ru.javawebinar.topjava.TimingRules; @ContextConfiguration({ "classpath:spring/spring-app.xml", @@ -26,33 +22,12 @@ @Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) @ActiveProfiles(resolver = ActiveDbProfileResolver.class) abstract public class AbstractServiceTest { - private static final Logger log = getLogger("result"); - - private static StringBuilder results = new StringBuilder(); - - // https://dzone.com/articles/applying-new-jdk-11-string-methods - private static String DELIM = "-".repeat(103); + @ClassRule + public static ExternalResource summary = TimingRules.SUMMARY; @Rule - public ExpectedException thrown = ExpectedException.none(); + public Stopwatch stopwatch = TimingRules.STOPWATCH; @Rule - // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev - public Stopwatch stopwatch = new Stopwatch() { - @Override - protected void finished(long nanos, Description description) { - String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos)); - results.append(result).append('\n'); - log.info(result + " ms\n"); - } - }; - - - @AfterClass - public static void printResult() { - log.info("\n" + DELIM + - "\nTest Duration, ms" + - "\n" + DELIM + "\n" + results + DELIM + "\n"); - results.setLength(0); - } + public ExpectedException thrown = ExpectedException.none(); } \ No newline at end of file From 81effb47c542551dd67e451c6464ca4534cf2d82 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:51:34 +0300 Subject: [PATCH 71/97] Applied patch - 6_04_HW5_optional_fix_jdbc_profiles --- .../repository/jdbc/JdbcMealRepository.java | 42 ++++++++++++++++--- .../javawebinar/topjava/web/MealServlet.java | 9 ++-- .../ru/javawebinar/topjava/SpringMain.java | 9 ++-- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index eafab7809baa..66286b6ae27f 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -1,6 +1,6 @@ package ru.javawebinar.topjava.repository.jdbc; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; @@ -9,14 +9,16 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.Profiles; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; +import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.List; @Repository -public class JdbcMealRepository implements MealRepository { +public abstract class JdbcMealRepository implements MealRepository { private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); @@ -26,8 +28,7 @@ public class JdbcMealRepository implements MealRepository { private final SimpleJdbcInsert insertMeal; - @Autowired - public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) .withTableName("meals") .usingGeneratedKeyColumns("id"); @@ -36,13 +37,42 @@ public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; } + + protected abstract T toDbDateTime(LocalDateTime ldt); + + @Repository + @Profile(Profiles.POSTGRES_DB) + public static class Java8JdbcMealRepository extends JdbcMealRepository { + public Java8JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(jdbcTemplate, namedParameterJdbcTemplate); + } + + @Override + protected LocalDateTime toDbDateTime(LocalDateTime ldt) { + return ldt; + } + } + + @Repository + @Profile(Profiles.HSQL_DB) + public static class TimestampJdbcMealRepository extends JdbcMealRepository { + public TimestampJdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(jdbcTemplate, namedParameterJdbcTemplate); + } + + @Override + protected Timestamp toDbDateTime(LocalDateTime ldt) { + return Timestamp.valueOf(ldt); + } + } + @Override public Meal save(Meal meal, int userId) { MapSqlParameterSource map = new MapSqlParameterSource() .addValue("id", meal.getId()) .addValue("description", meal.getDescription()) .addValue("calories", meal.getCalories()) - .addValue("date_time", meal.getDateTime()) + .addValue("date_time", toDbDateTime(meal.getDateTime())) .addValue("user_id", userId); if (meal.isNew()) { @@ -82,6 +112,6 @@ public List getAll(int userId) { public List getBetweenInclusive(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return jdbcTemplate.query( "SELECT * FROM meals WHERE user_id=? AND date_time >=? AND date_time < ? ORDER BY date_time DESC", - ROW_MAPPER, userId, startDateTime, endDateTime); + ROW_MAPPER, userId, toDbDateTime(startDateTime), toDbDateTime(endDateTime)); } } diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 919f3888d584..8716812f766c 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -1,8 +1,8 @@ package ru.javawebinar.topjava.web; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.util.StringUtils; +import ru.javawebinar.topjava.Profiles; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.web.meal.MealRestController; @@ -23,13 +23,16 @@ public class MealServlet extends HttpServlet { - private ConfigurableApplicationContext springContext; + private ClassPathXmlApplicationContext springContext; private MealRestController mealController; @Override public void init(ServletConfig config) throws ServletException { super.init(config); - springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/spring-db.xml"); + springContext = new ClassPathXmlApplicationContext(new String[]{"spring/spring-app.xml", "spring/spring-db.xml"}, false); +// springContext.setConfigLocations("spring/spring-app.xml", "spring/spring-db.xml"); + springContext.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION); + springContext.refresh(); mealController = springContext.getBean(MealRestController.class); } diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java index 15187b1f72f1..c0266272161f 100644 --- a/src/test/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -1,7 +1,6 @@ package ru.javawebinar.topjava; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.MealTo; @@ -17,7 +16,11 @@ public class SpringMain { public static void main(String[] args) { // java 7 automatic resource management - try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml")) { + try (GenericXmlApplicationContext appCtx = new GenericXmlApplicationContext()) { + appCtx.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION); + appCtx.load("spring/spring-app.xml", "spring/spring-db.xml"); + appCtx.refresh(); + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ROLE_ADMIN)); From 829b3bab4b4cbe6cc6cbbdc0bc9880005d9ccd4f Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:52:45 +0300 Subject: [PATCH 72/97] Applied patch - 6_05_update_hsqldb --- pom.xml | 2 +- .../repository/jdbc/JdbcMealRepository.java | 40 ++----------------- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/pom.xml b/pom.xml index e23c6fc0642a..0cb04a3ac00f 100644 --- a/pom.xml +++ b/pom.xml @@ -176,7 +176,7 @@ org.hsqldb hsqldb - 2.3.4 + 2.5.0 diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index 66286b6ae27f..afe75a982514 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -1,6 +1,5 @@ package ru.javawebinar.topjava.repository.jdbc; -import org.springframework.context.annotation.Profile; import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; @@ -9,16 +8,14 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.stereotype.Repository; -import ru.javawebinar.topjava.Profiles; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; -import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.List; @Repository -public abstract class JdbcMealRepository implements MealRepository { +public class JdbcMealRepository implements MealRepository { private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); @@ -28,7 +25,7 @@ public abstract class JdbcMealRepository implements MealRepository { private final SimpleJdbcInsert insertMeal; - JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) .withTableName("meals") .usingGeneratedKeyColumns("id"); @@ -37,42 +34,13 @@ public abstract class JdbcMealRepository implements MealRepository { this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; } - - protected abstract T toDbDateTime(LocalDateTime ldt); - - @Repository - @Profile(Profiles.POSTGRES_DB) - public static class Java8JdbcMealRepository extends JdbcMealRepository { - public Java8JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - super(jdbcTemplate, namedParameterJdbcTemplate); - } - - @Override - protected LocalDateTime toDbDateTime(LocalDateTime ldt) { - return ldt; - } - } - - @Repository - @Profile(Profiles.HSQL_DB) - public static class TimestampJdbcMealRepository extends JdbcMealRepository { - public TimestampJdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { - super(jdbcTemplate, namedParameterJdbcTemplate); - } - - @Override - protected Timestamp toDbDateTime(LocalDateTime ldt) { - return Timestamp.valueOf(ldt); - } - } - @Override public Meal save(Meal meal, int userId) { MapSqlParameterSource map = new MapSqlParameterSource() .addValue("id", meal.getId()) .addValue("description", meal.getDescription()) .addValue("calories", meal.getCalories()) - .addValue("date_time", toDbDateTime(meal.getDateTime())) + .addValue("date_time", meal.getDateTime()) .addValue("user_id", userId); if (meal.isNew()) { @@ -112,6 +80,6 @@ public List getAll(int userId) { public List getBetweenInclusive(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return jdbcTemplate.query( "SELECT * FROM meals WHERE user_id=? AND date_time >=? AND date_time < ? ORDER BY date_time DESC", - ROW_MAPPER, userId, toDbDateTime(startDateTime), toDbDateTime(endDateTime)); + ROW_MAPPER, userId, startDateTime, endDateTime); } } From c14a1ee76123ba746dcfc573e7f2b5459f9d83f5 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:53:48 +0300 Subject: [PATCH 73/97] Applied patch - 6_06_HW5_optional_fetch_join --- .../java/ru/javawebinar/topjava/model/User.java | 13 +++++++++---- .../topjava/repository/MealRepository.java | 4 ++++ .../topjava/repository/UserRepository.java | 4 ++++ .../repository/datajpa/CrudMealRepository.java | 3 +++ .../repository/datajpa/CrudUserRepository.java | 3 +++ .../datajpa/DataJpaMealRepository.java | 5 +++++ .../datajpa/DataJpaUserRepository.java | 5 +++++ .../topjava/service/MealService.java | 4 ++++ .../topjava/service/UserService.java | 4 ++++ .../ru/javawebinar/topjava/UserTestData.java | 4 ++-- .../service/AbstractMealServiceTest.java | 2 +- .../service/AbstractUserServiceTest.java | 2 +- .../service/datajpa/DataJpaMealServiceTest.java | 17 +++++++++++++++++ .../service/datajpa/DataJpaUserServiceTest.java | 16 ++++++++++++++++ 14 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 328cfc71869d..768e3360c29b 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -8,10 +8,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; -import java.util.Collection; -import java.util.Date; -import java.util.EnumSet; -import java.util.Set; +import java.util.*; import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; @@ -56,6 +53,10 @@ public class User extends AbstractNamedEntity { @Range(min = 10, max = 10000) private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") + @OrderBy("dateTime DESC") + protected List meals; + public User() { } @@ -125,6 +126,10 @@ 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{" + diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java index c633552ff220..5ef91549d260 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -31,4 +31,8 @@ default List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable // ORDERED dateTime desc List getBetweenInclusive(@NonNull LocalDateTime startDate, @NonNull 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 index 138369789175..9fecbddaaa19 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -18,4 +18,8 @@ public interface UserRepository { 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 index b04ad49bf463..c47d41275391 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java @@ -27,4 +27,7 @@ public interface CrudMealRepository extends JpaRepository { @Query("SELECT m from Meal m WHERE m.user.id=:userId AND m.dateTime >= :startDate AND m.dateTime < :endDate ORDER BY m.dateTime DESC") List 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 index 24c42a814220..8955fc4127b9 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -16,4 +16,7 @@ public interface CrudUserRepository extends JpaRepository { int delete(@Param("id") int id); User getByEmail(String email); + + @Query("SELECT u FROM User u LEFT JOIN FETCH u.meals WHERE u.id = ?1") + User getWithMeals(int id); } diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java index 68280d50537b..c8186f7896d8 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java @@ -47,4 +47,9 @@ public List getAll(int userId) { public List getBetweenInclusive(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return crudMealRepository.getBetween(startDateTime, endDateTime, userId); } + + @Override + public Meal getWithUser(int id, int userId) { + return crudMealRepository.getWithUser(id, userId); + } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java index 55ebf8a1098a..d016884f2668 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java @@ -39,4 +39,9 @@ public User getByEmail(String email) { 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/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 483118125769..090ed321ba1f 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -45,4 +45,8 @@ public Meal create(Meal meal, int userId) { Assert.notNull(meal, "meal must not be null"); return repository.save(meal, userId); } + + public Meal getWithUser(int id, int userId) { + return checkNotFoundWithId(repository.getWithUser(id, userId), id); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 1405b4757ea7..4510c44b4300 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -53,4 +53,8 @@ public void update(User user) { Assert.notNull(user, "user must not be null"); checkNotFoundWithId(repository.save(user), user.getId()); } + + public User getWithMeals(int id) { + return checkNotFoundWithId(repository.getWithMeals(id), id); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index bf27d417a1f1..086619eec978 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -29,7 +29,7 @@ public static User getUpdated() { } public static void assertMatch(User actual, User expected) { - assertThat(actual).isEqualToIgnoringGivenFields(expected, "registered", "roles"); + assertThat(actual).isEqualToIgnoringGivenFields(expected, "registered", "roles", "meals"); } public static void assertMatch(Iterable actual, User... expected) { @@ -37,6 +37,6 @@ public static void assertMatch(Iterable actual, User... expected) { } public static void assertMatch(Iterable actual, Iterable expected) { - assertThat(actual).usingElementComparatorIgnoringFields("registered", "roles").isEqualTo(expected); + assertThat(actual).usingElementComparatorIgnoringFields("registered", "roles", "meals").isEqualTo(expected); } } diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index 576da464b72c..ee8daf60bc23 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -15,7 +15,7 @@ public abstract class AbstractMealServiceTest extends AbstractServiceTest { @Autowired - private MealService service; + protected MealService service; @Test public void delete() throws Exception { diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index d250bef2bbac..10e367113caa 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -16,7 +16,7 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired - private UserService service; + protected UserService service; @Autowired private CacheManager cacheManager; diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java index e28cc6d6bc53..a1da4b23a08c 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -1,10 +1,27 @@ package ru.javawebinar.topjava.service.datajpa; +import org.junit.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 ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; @ActiveProfiles(DATAJPA) public class DataJpaMealServiceTest extends AbstractMealServiceTest { + @Test + public void getWithUser() throws Exception { + Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID); + assertMatch(adminMeal, ADMIN_MEAL1); + UserTestData.assertMatch(adminMeal.getUser(), UserTestData.ADMIN); + } + + @Test(expected = NotFoundException.class) + public void getWithUserNotFound() throws Exception { + service.getWithUser(1, 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 index a0757e739339..30be178dd9d5 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -1,10 +1,26 @@ package ru.javawebinar.topjava.service.datajpa; +import org.junit.Test; import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.AbstractUserServiceTest; +import ru.javawebinar.topjava.util.exception.NotFoundException; import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.*; @ActiveProfiles(DATAJPA) public class DataJpaUserServiceTest extends AbstractUserServiceTest { + @Test + public void getWithMeals() throws Exception { + User user = service.getWithMeals(USER_ID); + assertMatch(user, USER); + MealTestData.assertMatch(user.getMeals(), MealTestData.MEALS); + } + + @Test(expected = NotFoundException.class) + public void getWithMealsNotFound() throws Exception { + service.getWithMeals(1); + } } \ No newline at end of file From b7de2173e90cad5e2fca8246429a5875c80bfbe4 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:54:45 +0300 Subject: [PATCH 74/97] Applied patch - 6_07_HW5_graph_batch_size --- src/main/java/ru/javawebinar/topjava/model/User.java | 5 ++++- .../topjava/repository/datajpa/CrudUserRepository.java | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 768e3360c29b..0ecb4a400c34 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.model; +import org.hibernate.annotations.BatchSize; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; @@ -15,7 +16,7 @@ @NamedQueries({ @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), - @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email"), + @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")}) @@ -47,6 +48,8 @@ public class User extends AbstractNamedEntity { @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", nullable = false, columnDefinition = "int default 2000") diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java index 8955fc4127b9..806884a9c90b 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.repository.datajpa; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -11,12 +12,12 @@ public interface CrudUserRepository extends JpaRepository { @Transactional @Modifying -// @Query(name = User.DELETE) @Query("DELETE FROM User u WHERE u.id=:id") int delete(@Param("id") int id); User getByEmail(String email); - @Query("SELECT u FROM User u LEFT JOIN FETCH u.meals WHERE u.id = ?1") + @EntityGraph(attributePaths = {"meals", "roles"}) + @Query("SELECT u FROM User u WHERE u.id=?1") User getWithMeals(int id); } From 15a6232782a05a3e2be06bb1b1db1f309c1b6dc8 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 14:56:10 +0300 Subject: [PATCH 75/97] Applied patch - 6_08_add_test_validation --- .../javawebinar/topjava/util/ValidationUtil.java | 11 +++++++++++ .../topjava/service/AbstractMealServiceTest.java | 10 ++++++++++ .../topjava/service/AbstractServiceTest.java | 14 ++++++++++++++ .../topjava/service/AbstractUserServiceTest.java | 12 ++++++++++++ 4 files changed, 47 insertions(+) diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index cb51f3c60ac3..924b7f7205e9 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -42,4 +42,15 @@ public static void assureIdConsistent(AbstractBaseEntity entity, int id) { throw new IllegalArgumentException(entity + " 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; + } } \ 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 index ee8daf60bc23..2f64bdcf6511 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -5,9 +5,11 @@ import ru.javawebinar.topjava.model.Meal; 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 ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; import static ru.javawebinar.topjava.UserTestData.USER_ID; @@ -94,4 +96,12 @@ public void getBetween() throws Exception { public void getBetweenWithNullDates() throws Exception { assertMatch(service.getBetweenDates(null, null, USER_ID), MEALS); } + + @Test + public void createWithException() throws Exception { + 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 index d7a270369caa..338855f6af1c 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.service; +import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.rules.ExpectedException; @@ -14,6 +15,9 @@ import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.TimingRules; +import static org.hamcrest.CoreMatchers.instanceOf; +import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause; + @ContextConfiguration({ "classpath:spring/spring-app.xml", "classpath:spring/spring-db.xml" @@ -30,4 +34,14 @@ abstract public class AbstractServiceTest { @Rule public ExpectedException thrown = ExpectedException.none(); + + // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 + public void validateRootCause(Runnable runnable, Class exceptionClass) { + try { + runnable.run(); + Assert.fail("Expected " + exceptionClass.getName()); + } catch (Exception e) { + Assert.assertThat(getRootCause(e), instanceOf(exceptionClass)); + } + } } \ 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 index 10e367113caa..060731e5930e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -9,7 +9,10 @@ import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.util.exception.NotFoundException; +import javax.validation.ConstraintViolationException; +import java.util.Date; import java.util.List; +import java.util.Set; import static ru.javawebinar.topjava.UserTestData.*; @@ -81,4 +84,13 @@ public void getAll() throws Exception { List all = service.getAll(); assertMatch(all, ADMIN, USER); } + + @Test + public void createWithException() throws Exception { + validateRootCause(() -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", " ", "password", Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Set.of())), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Set.of())), ConstraintViolationException.class); + } } \ No newline at end of file From 3a3c5d78a1a4de7fdecca5ffb6235683bce547b5 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 15:00:07 +0300 Subject: [PATCH 76/97] Applied patch - 6_09_hibernate_cache --- pom.xml | 5 +++++ .../ru/javawebinar/topjava/model/User.java | 4 ++++ .../topjava/repository/JpaUtil.java | 22 +++++++++++++++++++ src/main/resources/spring/spring-db.xml | 8 +++++++ .../service/AbstractUserServiceTest.java | 5 +++++ 5 files changed, 44 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java diff --git a/pom.xml b/pom.xml index 0cb04a3ac00f..73083349ca44 100644 --- a/pom.xml +++ b/pom.xml @@ -106,6 +106,11 @@ hibernate-validator ${hibernate-validator.version} + + org.hibernate + hibernate-jcache + ${hibernate.version} + diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 0ecb4a400c34..86e8a0fe8d24 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.model; import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; @@ -13,6 +15,7 @@ import static ru.javawebinar.topjava.util.MealsUtil.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 u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), @@ -44,6 +47,7 @@ public class User extends AbstractNamedEntity { @NotNull 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") 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/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index a64af1dd224d..aee714dd11a5 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -73,6 +73,12 @@ + + + + + + @@ -87,6 +93,8 @@ + + diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 060731e5930e..ab2b0c6534a8 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -7,6 +7,7 @@ import org.springframework.dao.DataAccessException; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.JpaUtil; import ru.javawebinar.topjava.util.exception.NotFoundException; import javax.validation.ConstraintViolationException; @@ -24,9 +25,13 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired private CacheManager cacheManager; + @Autowired + protected JpaUtil jpaUtil; + @Before public void setUp() throws Exception { cacheManager.getCache("users").clear(); + jpaUtil.clear2ndLevelHibernateCache(); } @Test From d51a7caac81001d62031ad3147acf98d4bf998d4 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 15:00:40 +0300 Subject: [PATCH 77/97] Applied patch - 6_10_cascade_ddl --- .../ru/javawebinar/topjava/model/AbstractBaseEntity.java | 1 + src/main/java/ru/javawebinar/topjava/model/Meal.java | 3 +++ src/main/java/ru/javawebinar/topjava/model/User.java | 2 +- src/main/resources/spring/spring-db.xml | 6 ++++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index fefa8d7e6acf..f8f60b1f72d2 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -13,6 +13,7 @@ public abstract class AbstractBaseEntity implements Persistable { @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 diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index a87fca59ac96..15ae9c0033e3 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.model; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.validator.constraints.Range; import javax.persistence.*; @@ -40,6 +42,7 @@ public class Meal extends AbstractBaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) @NotNull private User user; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 86e8a0fe8d24..c2dbb517772a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -60,7 +60,7 @@ public class User extends AbstractNamedEntity { @Range(min = 10, max = 10000) private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; - @OneToMany(fetch = FetchType.LAZY, mappedBy = "user") + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") protected List meals; diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index aee714dd11a5..b53993e57446 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -79,6 +79,12 @@ + From 89171f1b6068dea12521ae61b4ec6b6402a35396 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 15:05:29 +0300 Subject: [PATCH 78/97] Applied patch - 6_11_spring_web --- pom.xml | 11 ++++++++--- .../ru/javawebinar/topjava/web/MealServlet.java | 16 +++------------- .../ru/javawebinar/topjava/web/UserServlet.java | 16 +++++++++++++++- src/main/webapp/WEB-INF/web.xml | 16 ++++++++++++++++ 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 73083349ca44..dec17b56f249 100644 --- a/pom.xml +++ b/pom.xml @@ -142,9 +142,9 @@ - javax.servlet - javax.servlet-api - 4.0.1 + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} provided @@ -154,6 +154,11 @@ 1.2 + + org.springframework + spring-web + + junit diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 8716812f766c..435c8dd13c43 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -1,8 +1,8 @@ package ru.javawebinar.topjava.web; -import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.util.StringUtils; -import ru.javawebinar.topjava.Profiles; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.web.meal.MealRestController; @@ -23,25 +23,15 @@ public class MealServlet extends HttpServlet { - private ClassPathXmlApplicationContext springContext; private MealRestController mealController; @Override public void init(ServletConfig config) throws ServletException { super.init(config); - springContext = new ClassPathXmlApplicationContext(new String[]{"spring/spring-app.xml", "spring/spring-db.xml"}, false); -// springContext.setConfigLocations("spring/spring-app.xml", "spring/spring-db.xml"); - springContext.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION); - springContext.refresh(); + WebApplicationContext springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); mealController = springContext.getBean(MealRestController.class); } - @Override - public void destroy() { - springContext.close(); - super.destroy(); - } - @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index 226023400c70..909e6e49a28f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -1,7 +1,11 @@ package ru.javawebinar.topjava.web; import org.slf4j.Logger; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import ru.javawebinar.topjava.web.user.AdminRestController; +import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -13,6 +17,15 @@ public class UserServlet extends HttpServlet { private static final Logger log = getLogger(UserServlet.class); + private AdminRestController adminController; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + WebApplicationContext springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); + adminController = springContext.getBean(AdminRestController.class); + } + @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int userId = Integer.parseInt(request.getParameter("userId")); @@ -22,7 +35,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.debug("forward to users"); + log.debug("getAll"); + request.setAttribute("users", adminController.getAll()); request.getRequestDispatcher("/users.jsp").forward(request, response); } } diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index bd98d3bf3f6a..0a3127acd44b 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -6,6 +6,22 @@ Topjava + + spring.profiles.default + postgres,datajpa + + + + contextConfigLocation + + classpath:spring/spring-app.xml + classpath:spring/spring-db.xml + + + + + org.springframework.web.context.ContextLoaderListener + userServlet ru.javawebinar.topjava.web.UserServlet From ca3d7381bb285f8050de927e1a6138d2d10dc9c5 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 15:05:58 +0300 Subject: [PATCH 79/97] Applied patch - 6_12_jsp_jstl_i18n --- src/main/resources/messages/app.properties | 12 ++++++ src/main/resources/messages/app_ru.properties | 12 ++++++ src/main/webapp/css/style.css | 8 ++++ src/main/webapp/fragments/bodyHeader.jsp | 5 +++ src/main/webapp/fragments/footer.jsp | 5 +++ src/main/webapp/fragments/headTag.jsp | 10 +++++ src/main/webapp/index.html | 18 -------- src/main/webapp/index.jsp | 23 +++++++++++ src/main/webapp/meals.jsp | 2 +- src/main/webapp/users.jsp | 41 ++++++++++++++++--- 10 files changed, 111 insertions(+), 25 deletions(-) create mode 100644 src/main/resources/messages/app.properties create mode 100644 src/main/resources/messages/app_ru.properties create mode 100644 src/main/webapp/fragments/bodyHeader.jsp create mode 100644 src/main/webapp/fragments/footer.jsp create mode 100644 src/main/webapp/fragments/headTag.jsp delete mode 100644 src/main/webapp/index.html create mode 100644 src/main/webapp/index.jsp diff --git a/src/main/resources/messages/app.properties b/src/main/resources/messages/app.properties new file mode 100644 index 000000000000..ccd4c66b5397 --- /dev/null +++ b/src/main/resources/messages/app.properties @@ -0,0 +1,12 @@ +app.title=Calories management +app.home=Home +app.footer=Internship Spring 5/JPA Enterprise (Topjava) application +app.login=Login as +user.title=Users +user.name=Name +user.email=Email +user.roles=Roles +user.active=Active +user.registered=Registered +meal.title=Meals +common.select=Select \ No newline at end of file diff --git a/src/main/resources/messages/app_ru.properties b/src/main/resources/messages/app_ru.properties new file mode 100644 index 000000000000..f22faf3cc513 --- /dev/null +++ b/src/main/resources/messages/app_ru.properties @@ -0,0 +1,12 @@ +app.title=Подсчет калорий +app.home=Главная +app.footer=Приложение стажировки Spring 5/JPA Enterprise (Topjava) +app.login=Зайти как +user.title=Пользователи +user.name=Имя +user.email=Почта +user.roles=Роли +user.active=Активный +user.registered=Зарегистрирован +meal.title=Моя еда +common.select=Выбрать \ No newline at end of file diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css index 26a14ce43e6d..ef7ff2124b98 100644 --- a/src/main/webapp/css/style.css +++ b/src/main/webapp/css/style.css @@ -22,3 +22,11 @@ tr[data-mealExcess="false"] { tr[data-mealExcess="true"] { color: red; } + +header, footer { + background: none repeat scroll 0 0 #A6C9E2; + color: #2E6E9E; + font-size: 20px; + padding: 5px 20px; + margin: 6px 0; +} diff --git a/src/main/webapp/fragments/bodyHeader.jsp b/src/main/webapp/fragments/bodyHeader.jsp new file mode 100644 index 000000000000..c165b9b8bb42 --- /dev/null +++ b/src/main/webapp/fragments/bodyHeader.jsp @@ -0,0 +1,5 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + +
    | |
    \ No newline at end of file diff --git a/src/main/webapp/fragments/footer.jsp b/src/main/webapp/fragments/footer.jsp new file mode 100644 index 000000000000..c59bff2e54bf --- /dev/null +++ b/src/main/webapp/fragments/footer.jsp @@ -0,0 +1,5 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + +
    +
    \ No newline at end of file diff --git a/src/main/webapp/fragments/headTag.jsp b/src/main/webapp/fragments/headTag.jsp new file mode 100644 index 000000000000..c1532783af5e --- /dev/null +++ b/src/main/webapp/fragments/headTag.jsp @@ -0,0 +1,10 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> + + + + + <fmt:message key="app.title"/> + + \ No newline at end of file diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html deleted file mode 100644 index 886449733a86..000000000000 --- a/src/main/webapp/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - Java Enterprise (Topjava) - - -

    Проект Java Enterprise (Topjava)

    -
    -
    - Meals of  - - -
    - - diff --git a/src/main/webapp/index.jsp b/src/main/webapp/index.jsp new file mode 100644 index 000000000000..0e5a407ae621 --- /dev/null +++ b/src/main/webapp/index.jsp @@ -0,0 +1,23 @@ +<%@ 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" %> + + + + + + + +
    +
    +
    + : + + +
    + + + \ No newline at end of file diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index 16c4b1e16d62..c81579d5f745 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -9,7 +9,7 @@
    -

    Home

    +

    Home


    Meals

    diff --git a/src/main/webapp/users.jsp b/src/main/webapp/users.jsp index 60bdc9b83192..ef6f7d1d249e 100644 --- a/src/main/webapp/users.jsp +++ b/src/main/webapp/users.jsp @@ -1,11 +1,40 @@ <%@ 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" %> + + + - - Users - + -

    Home

    -
    -

    Users

    + + +
    +

    + + + + + + + + + + + + + + + + + + + + + +
    ${user.email}${user.roles}<%=user.isEnabled()%> +
    +
    + \ No newline at end of file From a436039c8e6370dccd3146fa3697a43c26aa46fa Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 15:13:48 +0300 Subject: [PATCH 80/97] Applied patch - 6_13_tomcat_pool_jndi_cargo --- pom.xml | 42 ++++++++++++++++++ src/main/resources/db/tomcat.properties | 5 +++ src/main/resources/spring/spring-db.xml | 9 +++- src/main/resources/tomcat/context.xml | 57 +++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/tomcat.properties create mode 100644 src/main/resources/tomcat/context.xml diff --git a/pom.xml b/pom.xml index dec17b56f249..a9e76d84f28c 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,48 @@ -Dfile.encoding=UTF-8 + + + + + org.codehaus.cargo + cargo-maven2-plugin + 1.7.7 + + + 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} + + + + + 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/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index b53993e57446..d26ec2d0a361 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -5,11 +5,13 @@ xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa" + xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd - http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> + http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd + http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd"> @@ -50,6 +52,11 @@ p:password="${database.password}"/> + + + + + 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 + + + + + + + + + From aab7d861b2cff76489254d2567118d95a2dab973 Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 15:14:18 +0300 Subject: [PATCH 81/97] Applied patch - 6_14_spring_webmvc --- pom.xml | 2 +- .../topjava/web/RootController.java | 34 +++++++++++++++++++ src/main/resources/logback.xml | 1 + src/main/resources/spring/spring-app.xml | 3 -- src/main/resources/spring/spring-mvc.xml | 18 ++++++++++ .../WEB-INF/jsp/fragments/bodyHeader.jsp | 7 ++++ .../{ => WEB-INF/jsp}/fragments/footer.jsp | 0 .../{ => WEB-INF/jsp}/fragments/headTag.jsp | 2 +- src/main/webapp/{ => WEB-INF/jsp}/index.jsp | 0 .../webapp/{ => WEB-INF/jsp}/mealForm.jsp | 0 src/main/webapp/{ => WEB-INF/jsp}/meals.jsp | 0 src/main/webapp/{ => WEB-INF/jsp}/users.jsp | 0 src/main/webapp/WEB-INF/web.xml | 26 ++++++-------- src/main/webapp/fragments/bodyHeader.jsp | 5 --- src/main/webapp/{ => resources}/css/style.css | 0 15 files changed, 72 insertions(+), 26 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/RootController.java create mode 100644 src/main/resources/spring/spring-mvc.xml create mode 100644 src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp rename src/main/webapp/{ => WEB-INF/jsp}/fragments/footer.jsp (100%) rename src/main/webapp/{ => WEB-INF/jsp}/fragments/headTag.jsp (85%) rename src/main/webapp/{ => WEB-INF/jsp}/index.jsp (100%) rename src/main/webapp/{ => WEB-INF/jsp}/mealForm.jsp (100%) rename src/main/webapp/{ => WEB-INF/jsp}/meals.jsp (100%) rename src/main/webapp/{ => WEB-INF/jsp}/users.jsp (100%) delete mode 100644 src/main/webapp/fragments/bodyHeader.jsp rename src/main/webapp/{ => resources}/css/style.css (100%) diff --git a/pom.xml b/pom.xml index a9e76d84f28c..5fefb5eed804 100644 --- a/pom.xml +++ b/pom.xml @@ -198,7 +198,7 @@ org.springframework - spring-web + spring-webmvc 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..32726bc8c84e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -0,0 +1,34 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import ru.javawebinar.topjava.service.UserService; + +import javax.servlet.http.HttpServletRequest; + +@Controller +public class RootController { + @Autowired + private UserService service; + + @GetMapping("/") + public String root() { + return "index"; + } + + @GetMapping("/users") + public String getUsers(Model model) { + model.addAttribute("users", service.getAll()); + return "users"; + } + + @PostMapping("/users") + public String setUser(HttpServletRequest request) { + int userId = Integer.parseInt(request.getParameter("userId")); + SecurityUtil.setAuthUserId(userId); + return "redirect:meals"; + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 12a4f63c6c15..809d4c9c3d50 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -21,6 +21,7 @@ + diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 8aa867cc15e2..77499580a745 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -17,7 +17,4 @@ - - - \ 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..7c71d5af5939 --- /dev/null +++ b/src/main/resources/spring/spring-mvc.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + \ 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..888df90f2527 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -0,0 +1,7 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + +
    + | | +
    \ No newline at end of file diff --git a/src/main/webapp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp similarity index 100% rename from src/main/webapp/fragments/footer.jsp rename to src/main/webapp/WEB-INF/jsp/fragments/footer.jsp diff --git a/src/main/webapp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp similarity index 85% rename from src/main/webapp/fragments/headTag.jsp rename to src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index c1532783af5e..da40d461d76a 100644 --- a/src/main/webapp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -6,5 +6,5 @@ <fmt:message key="app.title"/> - + \ No newline at end of file diff --git a/src/main/webapp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp similarity index 100% rename from src/main/webapp/index.jsp rename to src/main/webapp/WEB-INF/jsp/index.jsp diff --git a/src/main/webapp/mealForm.jsp b/src/main/webapp/WEB-INF/jsp/mealForm.jsp similarity index 100% rename from src/main/webapp/mealForm.jsp rename to src/main/webapp/WEB-INF/jsp/mealForm.jsp diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp similarity index 100% rename from src/main/webapp/meals.jsp rename to src/main/webapp/WEB-INF/jsp/meals.jsp diff --git a/src/main/webapp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp similarity index 100% rename from src/main/webapp/users.jsp rename to src/main/webapp/WEB-INF/jsp/users.jsp diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 0a3127acd44b..b7558a705aa6 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -19,27 +19,21 @@ + org.springframework.web.context.ContextLoaderListener - userServlet - ru.javawebinar.topjava.web.UserServlet - 0 + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:spring/spring-mvc.xml + + 1 - userServlet - /users + mvc-dispatcher + / - - - mealServlet - ru.javawebinar.topjava.web.MealServlet - 0 - - - mealServlet - /meals - - diff --git a/src/main/webapp/fragments/bodyHeader.jsp b/src/main/webapp/fragments/bodyHeader.jsp deleted file mode 100644 index c165b9b8bb42..000000000000 --- a/src/main/webapp/fragments/bodyHeader.jsp +++ /dev/null @@ -1,5 +0,0 @@ -<%@page contentType="text/html" pageEncoding="UTF-8" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - - -
    | |
    \ No newline at end of file diff --git a/src/main/webapp/css/style.css b/src/main/webapp/resources/css/style.css similarity index 100% rename from src/main/webapp/css/style.css rename to src/main/webapp/resources/css/style.css From 6b735cb98e0dd4264415b53fb57533e71a03da3e Mon Sep 17 00:00:00 2001 From: alkone Date: Fri, 15 Nov 2019 15:32:45 +0300 Subject: [PATCH 82/97] Applied patch - 6_15_spring_i18n --- .../messages/app.properties | 0 .../messages/app_ru.properties | 0 src/main/resources/spring/spring-mvc.xml | 18 ++++++++++++++++++ .../WEB-INF/jsp/fragments/bodyHeader.jsp | 5 ++--- .../webapp/WEB-INF/jsp/fragments/footer.jsp | 5 ++--- .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 5 ++--- src/main/webapp/WEB-INF/jsp/index.jsp | 8 +++----- src/main/webapp/WEB-INF/jsp/users.jsp | 15 +++++++-------- 8 files changed, 34 insertions(+), 22 deletions(-) rename {src/main/resources => config}/messages/app.properties (100%) rename {src/main/resources => config}/messages/app_ru.properties (100%) diff --git a/src/main/resources/messages/app.properties b/config/messages/app.properties similarity index 100% rename from src/main/resources/messages/app.properties rename to config/messages/app.properties diff --git a/src/main/resources/messages/app_ru.properties b/config/messages/app_ru.properties similarity index 100% rename from src/main/resources/messages/app_ru.properties rename to config/messages/app_ru.properties diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 7c71d5af5939..ce12ddcc64d5 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -15,4 +15,22 @@ + + + + + + \ 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 index 888df90f2527..5b5efe57e65b 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -1,7 +1,6 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> -<%@ 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/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp index c59bff2e54bf..0935c441a36b 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp @@ -1,5 +1,4 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> -<%@ 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 +
    \ 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 index da40d461d76a..6d77694e3406 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -1,10 +1,9 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> - - <fmt:message key="app.title"/> + <spring:message code="app.title"/> \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp index 0e5a407ae621..faf5692f656b 100644 --- a/src/main/webapp/WEB-INF/jsp/index.jsp +++ b/src/main/webapp/WEB-INF/jsp/index.jsp @@ -1,8 +1,6 @@ <%@ 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" %> @@ -11,11 +9,11 @@
    - : - +
    diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index ef6f7d1d249e..e4aa345f4787 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -1,8 +1,7 @@ <%@ 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" %> @@ -10,16 +9,16 @@
    -

    +

    - - - - - + + + + + From 122875c72d738bdb8adfe49973924a4c2e3a943b Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:13:28 +0300 Subject: [PATCH 83/97] Applied patch - 7_0_refactoring --- .../javawebinar/topjava/util/DateTimeUtil.java | 8 ++++---- .../InMemoryAdminRestControllerSpringTest.java | 15 ++++----------- .../user/InMemoryAdminRestControllerTest.java | 18 ++++++++---------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 61ebc14682e4..0ff7a24f62c7 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -13,8 +13,8 @@ public class DateTimeUtil { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); // HSQLDB 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 static final LocalDateTime MIN_DATE = LocalDate.of(1, 1, 1).atStartOfDay(); + private static final LocalDateTime MAX_DATE = LocalDate.of(3000, 1, 1).atStartOfDay(); private DateTimeUtil() { } @@ -32,11 +32,11 @@ public static String toString(LocalDateTime ldt) { } public static LocalDateTime getStartInclusive(LocalDate localDate) { - return (localDate != null ? localDate : MIN_DATE).atStartOfDay(); + return localDate != null ? localDate.atStartOfDay() : MIN_DATE; } public static LocalDateTime getEndExclusive(LocalDate localDate) { - return (localDate != null ? localDate.plus(1, ChronoUnit.DAYS) : MAX_DATE).atStartOfDay(); + return localDate != null ? localDate.plus(1, ChronoUnit.DAYS).atStartOfDay() : MAX_DATE; } } diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java index b535e7c261f1..872a3b1736c9 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -1,20 +1,15 @@ package ru.javawebinar.topjava.web.user; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; -import ru.javawebinar.topjava.UserTestData; -import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository; import ru.javawebinar.topjava.util.exception.NotFoundException; -import java.util.Collection; - -import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER_ID; @ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) @RunWith(SpringRunner.class) @@ -31,12 +26,10 @@ public void setUp() throws Exception { repository.init(); } - @Test + @Test(expected = NotFoundException.class) public void delete() throws Exception { - controller.delete(UserTestData.USER_ID); - Collection users = controller.getAll(); - Assert.assertEquals(1, users.size()); - Assert.assertEquals(ADMIN, users.iterator().next()); + controller.delete(USER_ID); + controller.get(USER_ID); } @Test(expected = NotFoundException.class) diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index bf89efacfe52..150563a97c87 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -1,19 +1,19 @@ package ru.javawebinar.topjava.web.user; -import org.junit.*; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.InMemoryUserRepository; import ru.javawebinar.topjava.util.exception.NotFoundException; import java.util.Arrays; -import java.util.Collection; -import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER_ID; public class InMemoryAdminRestControllerTest { private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); @@ -40,12 +40,10 @@ public void setUp() throws Exception { repository.init(); } - @Test + @Test(expected = NotFoundException.class) public void delete() throws Exception { - controller.delete(UserTestData.USER_ID); - Collection users = controller.getAll(); - Assert.assertEquals(1, users.size()); - Assert.assertEquals(ADMIN, users.iterator().next()); + controller.delete(USER_ID); + controller.get(USER_ID); } @Test(expected = NotFoundException.class) From 38c429b5d6d770e767b3f7ec612107f51167bc0d Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:14:23 +0300 Subject: [PATCH 84/97] Applied patch - 7_01_HW6_fix_tests --- .../ru/javawebinar/topjava/SpringMain.java | 2 +- .../service/AbstractJpaUserServiceTest.java | 34 +++++++++++++++++++ .../service/AbstractMealServiceTest.java | 2 ++ .../topjava/service/AbstractServiceTest.java | 11 ++++++ .../service/AbstractUserServiceTest.java | 17 ---------- .../datajpa/DataJpaUserServiceTest.java | 4 +-- .../service/jpa/JpaUserServiceTest.java | 4 +-- .../user/InMemoryAdminRestControllerTest.java | 4 ++- src/test/resources/spring/inmemory.xml | 1 + 9 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java index c0266272161f..bd02c9b01821 100644 --- a/src/test/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -18,7 +18,7 @@ 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/spring-db.xml"); + appCtx.load("spring/spring-app.xml", "spring/inmemory.xml"); appCtx.refresh(); System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); 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..753e2b10f396 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java @@ -0,0 +1,34 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Before; +import org.junit.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.Date; +import java.util.Set; + +abstract public class AbstractJpaUserServiceTest extends AbstractUserServiceTest { + + @Autowired + private JpaUtil jpaUtil; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + jpaUtil.clear2ndLevelHibernateCache(); + } + + @Test + public void createWithException() throws Exception { + validateRootCause(() -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", " ", "password", Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.ROLE_USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Set.of())), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Set.of())), 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 index 2f64bdcf6511..1e2e82ddd6a7 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.service; +import org.junit.Assume; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import ru.javawebinar.topjava.model.Meal; @@ -99,6 +100,7 @@ public void getBetweenWithNullDates() throws Exception { @Test public void createWithException() throws Exception { + Assume.assumeTrue(isJpaBased()); 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); diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index 338855f6af1c..97ecc308f00d 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -7,12 +7,15 @@ import org.junit.rules.ExternalResource; import org.junit.rules.Stopwatch; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; import org.springframework.test.context.junit4.SpringRunner; import ru.javawebinar.topjava.ActiveDbProfileResolver; +import ru.javawebinar.topjava.Profiles; import ru.javawebinar.topjava.TimingRules; import static org.hamcrest.CoreMatchers.instanceOf; @@ -32,9 +35,17 @@ abstract public class AbstractServiceTest { @Rule public Stopwatch stopwatch = TimingRules.STOPWATCH; + @Autowired + public Environment env; + @Rule public ExpectedException thrown = ExpectedException.none(); + public 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 public void validateRootCause(Runnable runnable, Class exceptionClass) { try { diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index ab2b0c6534a8..10e367113caa 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -7,13 +7,9 @@ import org.springframework.dao.DataAccessException; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; -import ru.javawebinar.topjava.repository.JpaUtil; import ru.javawebinar.topjava.util.exception.NotFoundException; -import javax.validation.ConstraintViolationException; -import java.util.Date; import java.util.List; -import java.util.Set; import static ru.javawebinar.topjava.UserTestData.*; @@ -25,13 +21,9 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired private CacheManager cacheManager; - @Autowired - protected JpaUtil jpaUtil; - @Before public void setUp() throws Exception { cacheManager.getCache("users").clear(); - jpaUtil.clear2ndLevelHibernateCache(); } @Test @@ -89,13 +81,4 @@ public void getAll() throws Exception { List all = service.getAll(); assertMatch(all, ADMIN, USER); } - - @Test - public void createWithException() throws Exception { - validateRootCause(() -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.ROLE_USER)), ConstraintViolationException.class); - validateRootCause(() -> service.create(new User(null, "User", " ", "password", Role.ROLE_USER)), ConstraintViolationException.class); - validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.ROLE_USER)), ConstraintViolationException.class); - validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Set.of())), ConstraintViolationException.class); - validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Set.of())), ConstraintViolationException.class); - } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java index 30be178dd9d5..ca48cdcdfd04 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -4,14 +4,14 @@ import org.springframework.test.context.ActiveProfiles; import ru.javawebinar.topjava.MealTestData; import ru.javawebinar.topjava.model.User; -import ru.javawebinar.topjava.service.AbstractUserServiceTest; +import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest; import ru.javawebinar.topjava.util.exception.NotFoundException; import static ru.javawebinar.topjava.Profiles.DATAJPA; import static ru.javawebinar.topjava.UserTestData.*; @ActiveProfiles(DATAJPA) -public class DataJpaUserServiceTest extends AbstractUserServiceTest { +public class DataJpaUserServiceTest extends AbstractJpaUserServiceTest { @Test public void getWithMeals() throws Exception { User user = service.getWithMeals(USER_ID); diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java index d1b3e4699785..ffb1fea03c22 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java @@ -1,10 +1,10 @@ package ru.javawebinar.topjava.service.jpa; import org.springframework.test.context.ActiveProfiles; -import ru.javawebinar.topjava.service.AbstractUserServiceTest; +import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest; import static ru.javawebinar.topjava.Profiles.JPA; @ActiveProfiles(JPA) -public class JpaUserServiceTest extends AbstractUserServiceTest { +public class JpaUserServiceTest extends AbstractJpaUserServiceTest { } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index 150563a97c87..67c295c02172 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -30,7 +30,9 @@ public static void beforeClass() { @AfterClass public static void afterClass() { - appCtx.close(); +// 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(); } @Before diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml index c6a2710cbd88..5798d6f5ac1c 100644 --- a/src/test/resources/spring/inmemory.xml +++ b/src/test/resources/spring/inmemory.xml @@ -4,4 +4,5 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> + \ No newline at end of file From a22da31d60fe023f57d85c35391b57eb558313b3 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:16:46 +0300 Subject: [PATCH 85/97] Applied patch - 7_02_HW6_meals --- config/messages/app.properties | 19 +++- config/messages/app_ru.properties | 19 +++- .../javawebinar/topjava/web/MealServlet.java | 89 ------------------- .../topjava/web/RootController.java | 16 +++- .../javawebinar/topjava/web/UserServlet.java | 42 --------- .../web/meal/AbstractMealController.java | 72 +++++++++++++++ .../topjava/web/meal/JspMealController.java | 70 +++++++++++++++ .../topjava/web/meal/MealRestController.java | 73 +-------------- src/main/webapp/WEB-INF/jsp/mealForm.jsp | 24 ++--- src/main/webapp/WEB-INF/jsp/meals.jsp | 46 +++++----- 10 files changed, 227 insertions(+), 243 deletions(-) delete mode 100644 src/main/java/ru/javawebinar/topjava/web/MealServlet.java delete mode 100644 src/main/java/ru/javawebinar/topjava/web/UserServlet.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java diff --git a/config/messages/app.properties b/config/messages/app.properties index ccd4c66b5397..ca6c456226e9 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -2,11 +2,28 @@ app.title=Calories management app.home=Home app.footer=Internship Spring 5/JPA Enterprise (Topjava) application app.login=Login as + user.title=Users user.name=Name user.email=Email user.roles=Roles user.active=Active user.registered=Registered + meal.title=Meals -common.select=Select \ No newline at end of file +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.select=Select +common.delete=Delete +common.update=Update +common.save=Save +common.cancel=Cancel \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index f22faf3cc513..bc98b98f4b33 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -2,11 +2,28 @@ app.title=Подсчет калорий app.home=Главная app.footer=Приложение стажировки Spring 5/JPA Enterprise (Topjava) app.login=Зайти как + user.title=Пользователи user.name=Имя user.email=Почта user.roles=Роли user.active=Активный user.registered=Зарегистрирован + meal.title=Моя еда -common.select=Выбрать \ No newline at end of file +meal.edit=Редактирование еды +meal.add=Добавление еды +meal.filter=Отфильтровать +meal.startDate=От даты +meal.endDate=До даты +meal.startTime=От времени +meal.endTime=До времени +meal.description=Описание +meal.dateTime=Дата/Время +meal.calories=Калории + +common.select=Выбрать +common.delete=Удалить +common.update=Обновить +common.save=Сохранить +common.cancel=Отменить \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java deleted file mode 100644 index 435c8dd13c43..000000000000 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ /dev/null @@ -1,89 +0,0 @@ -package ru.javawebinar.topjava.web; - -import org.springframework.util.StringUtils; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; -import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.web.meal.MealRestController; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.temporal.ChronoUnit; -import java.util.Objects; - -import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; -import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; - -public class MealServlet extends HttpServlet { - - private MealRestController mealController; - - @Override - public void init(ServletConfig config) throws ServletException { - super.init(config); - WebApplicationContext springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); - mealController = springContext.getBean(MealRestController.class); - } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - request.setCharacterEncoding("UTF-8"); - Meal meal = new Meal( - LocalDateTime.parse(request.getParameter("dateTime")), - request.getParameter("description"), - Integer.parseInt(request.getParameter("calories"))); - - if (StringUtils.isEmpty(request.getParameter("id"))) { - mealController.create(meal); - } else { - mealController.update(meal, getId(request)); - } - response.sendRedirect("meals"); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String action = request.getParameter("action"); - - switch (action == null ? "all" : action) { - case "delete": - int id = getId(request); - mealController.delete(id); - response.sendRedirect("meals"); - break; - case "create": - case "update": - final Meal meal = "create".equals(action) ? - new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : - mealController.get(getId(request)); - request.setAttribute("meal", meal); - request.getRequestDispatcher("/mealForm.jsp").forward(request, response); - break; - case "filter": - LocalDate startDate = parseLocalDate(request.getParameter("startDate")); - LocalDate endDate = parseLocalDate(request.getParameter("endDate")); - LocalTime startTime = parseLocalTime(request.getParameter("startTime")); - LocalTime endTime = parseLocalTime(request.getParameter("endTime")); - request.setAttribute("meals", mealController.getBetween(startDate, startTime, endDate, endTime)); - request.getRequestDispatcher("/meals.jsp").forward(request, response); - break; - case "all": - default: - request.setAttribute("meals", mealController.getAll()); - request.getRequestDispatcher("/meals.jsp").forward(request, response); - break; - } - } - - private int getId(HttpServletRequest request) { - String paramId = Objects.requireNonNull(request.getParameter("id")); - return Integer.parseInt(paramId); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java index 32726bc8c84e..8081c8ba791a 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -5,14 +5,19 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import ru.javawebinar.topjava.service.MealService; import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.util.MealsUtil; import javax.servlet.http.HttpServletRequest; @Controller public class RootController { @Autowired - private UserService service; + private UserService userService; + + @Autowired + private MealService mealService; @GetMapping("/") public String root() { @@ -21,7 +26,7 @@ public String root() { @GetMapping("/users") public String getUsers(Model model) { - model.addAttribute("users", service.getAll()); + model.addAttribute("users", userService.getAll()); return "users"; } @@ -31,4 +36,11 @@ public String setUser(HttpServletRequest request) { SecurityUtil.setAuthUserId(userId); return "redirect:meals"; } + + @GetMapping("/meals") + public String getMeals(Model model) { + model.addAttribute("meals", + MealsUtil.getTos(mealService.getAll(SecurityUtil.authUserId()), SecurityUtil.authUserCaloriesPerDay())); + return "meals"; + } } diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java deleted file mode 100644 index 909e6e49a28f..000000000000 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ /dev/null @@ -1,42 +0,0 @@ -package ru.javawebinar.topjava.web; - -import org.slf4j.Logger; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; -import ru.javawebinar.topjava.web.user.AdminRestController; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -import static org.slf4j.LoggerFactory.getLogger; - -public class UserServlet extends HttpServlet { - private static final Logger log = getLogger(UserServlet.class); - - private AdminRestController adminController; - - @Override - public void init(ServletConfig config) throws ServletException { - super.init(config); - WebApplicationContext springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()); - adminController = springContext.getBean(AdminRestController.class); - } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - int userId = Integer.parseInt(request.getParameter("userId")); - SecurityUtil.setAuthUserId(userId); - response.sendRedirect("meals"); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.debug("getAll"); - request.setAttribute("users", adminController.getAll()); - request.getRequestDispatcher("/users.jsp").forward(request, response); - } -} 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..1f1b4e587431 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java @@ -0,0 +1,72 @@ +package ru.javawebinar.topjava.web.meal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.Nullable; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.web.SecurityUtil; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.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.getTos(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(@Nullable LocalDate startDate, @Nullable LocalTime startTime, + @Nullable LocalDate endDate, @Nullable LocalTime endTime) { + int userId = SecurityUtil.authUserId(); + log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); + + List mealsDateFiltered = service.getBetweenDates(startDate, endDate, userId); + return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java new file mode 100644 index 000000000000..0603be3f6977 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java @@ -0,0 +1,70 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import ru.javawebinar.topjava.model.Meal; + +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; + +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; + +@Controller +@RequestMapping(value = "/meals") +public class JspMealController extends AbstractMealController { + + @GetMapping("/delete") + public String delete(HttpServletRequest request) { + super.delete(getId(request)); + return "redirect:meals"; + } + + @GetMapping("/update") + public String update(HttpServletRequest request, Model model) { + model.addAttribute("meal", super.get(getId(request))); + return "mealForm"; + } + + @GetMapping("/create") + public String create(Model model) { + model.addAttribute("meal", new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS), "", 1000)); + return "mealForm"; + } + + @PostMapping + public String updateOrCreate(HttpServletRequest request) { + Meal meal = new Meal(LocalDateTime.parse(request.getParameter("dateTime")), + request.getParameter("description"), + Integer.valueOf(request.getParameter("calories"))); + + if (request.getParameter("id").isEmpty()) { + super.create(meal); + } else { + super.update(meal, getId(request)); + } + return "redirect:meals"; + } + + @GetMapping("/filter") + public String getBetween(HttpServletRequest request, Model model) { + LocalDate startDate = parseLocalDate(request.getParameter("startDate")); + LocalDate endDate = parseLocalDate(request.getParameter("endDate")); + LocalTime startTime = parseLocalTime(request.getParameter("startTime")); + LocalTime endTime = parseLocalTime(request.getParameter("endTime")); + model.addAttribute("meals", super.getBetween(startDate, startTime, endDate, endTime)); + return "meals"; + } + + private int getId(HttpServletRequest request) { + String paramId = Objects.requireNonNull(request.getParameter("id")); + return Integer.valueOf(paramId); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index cddd5acee7b7..c3daf68535d7 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -1,78 +1,7 @@ package ru.javawebinar.topjava.web.meal; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; -import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.service.MealService; -import ru.javawebinar.topjava.to.MealTo; -import ru.javawebinar.topjava.util.MealsUtil; -import ru.javawebinar.topjava.web.SecurityUtil; - -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.List; - -import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; -import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; @Controller -public class MealRestController { - private static final Logger log = LoggerFactory.getLogger(MealRestController.class); - - private final MealService service; - - @Autowired - public MealRestController(MealService service) { - this.service = service; - } - - public Meal get(int id) { - int userId = SecurityUtil.authUserId(); - log.info("get meal {} for user {}", id, userId); - return service.get(id, userId); - } - - public void delete(int id) { - int userId = SecurityUtil.authUserId(); - log.info("delete meal {} for user {}", id, userId); - service.delete(id, userId); - } - - public List getAll() { - int userId = SecurityUtil.authUserId(); - log.info("getAll for user {}", userId); - return MealsUtil.getTos(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay()); - } - - public Meal create(Meal meal) { - int userId = SecurityUtil.authUserId(); - 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(@Nullable LocalDate startDate, @Nullable LocalTime startTime, - @Nullable LocalDate endDate, @Nullable LocalTime endTime) { - int userId = SecurityUtil.authUserId(); - log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); - - List mealsDateFiltered = service.getBetweenDates(startDate, endDate, userId); - return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); - } +public class MealRestController extends AbstractMealController { } \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/mealForm.jsp b/src/main/webapp/WEB-INF/jsp/mealForm.jsp index e9c01a94c999..999c1e216ddb 100644 --- a/src/main/webapp/WEB-INF/jsp/mealForm.jsp +++ b/src/main/webapp/WEB-INF/jsp/mealForm.jsp @@ -1,34 +1,34 @@ <%@ 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" %> - - Meal - - + + +
    -

    Home

    -
    -

    ${param.action == 'create' ? 'Create meal' : 'Edit meal'}

    +

    +
    -
    DateTime:
    +
    :
    -
    Description:
    +
    :
    -
    Calories:
    +
    :
    - - + +
    + diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp index c81579d5f745..4d31a637f9dd 100644 --- a/src/main/webapp/WEB-INF/jsp/meals.jsp +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -1,52 +1,49 @@ <%@ 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" %> <%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %> - - Meals - - + + +
    -

    Home

    -
    -

    Meals

    -
    - +

    + +
    -
    From Date:
    +
    :
    -
    To Date:
    +
    :
    -
    From Time:
    +
    :
    -
    To Time:
    +
    :
    - + -
    - Add Meal -

    +
    + +
    - - - + + + - + - - + +
    DateDescriptionCalories
    <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> @@ -56,11 +53,12 @@ ${meal.description} ${meal.calories}UpdateDelete
    + \ No newline at end of file From 9c02e71f63580d1dfd1bb4a7e933924d829d2309 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:17:29 +0300 Subject: [PATCH 86/97] Applied patch - 7_03_HW6_fix_relative_url_utf8 --- .../topjava/web/meal/JspMealController.java | 4 ++-- .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 1 + src/main/webapp/WEB-INF/web.xml | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java index 0603be3f6977..59cf8b3944d5 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java @@ -24,7 +24,7 @@ public class JspMealController extends AbstractMealController { @GetMapping("/delete") public String delete(HttpServletRequest request) { super.delete(getId(request)); - return "redirect:meals"; + return "redirect:/meals"; } @GetMapping("/update") @@ -50,7 +50,7 @@ public String updateOrCreate(HttpServletRequest request) { } else { super.update(meal, getId(request)); } - return "redirect:meals"; + return "redirect:/meals"; } @GetMapping("/filter") diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 6d77694e3406..0c77f10859f8 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -5,5 +5,6 @@ <spring:message code="app.title"/> + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index b7558a705aa6..175ec350e9d1 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -36,4 +36,21 @@ mvc-dispatcher / + + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + From 4ab9aaf75d93998f9d478d7eda4398dd460bfbc6 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:19:00 +0300 Subject: [PATCH 87/97] Applied patch - 7_04_HW6_optional_add_role --- src/main/java/ru/javawebinar/topjava/model/User.java | 2 +- src/main/resources/db/populateDB.sql | 3 ++- src/test/java/ru/javawebinar/topjava/UserTestData.java | 7 ++++--- .../topjava/service/AbstractUserServiceTest.java | 8 ++++---- .../topjava/service/datajpa/DataJpaUserServiceTest.java | 6 +++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index c2dbb517772a..3cf7408b8dea 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -18,7 +18,7 @@ @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 u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), + @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 diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql index 3a9ed84b9cdf..b09a236081cb 100644 --- a/src/main/resources/db/populateDB.sql +++ b/src/main/resources/db/populateDB.sql @@ -9,7 +9,8 @@ INSERT INTO users (name, email, password) VALUES INSERT INTO user_roles (role, user_id) VALUES ('ROLE_USER', 100000), - ('ROLE_ADMIN', 100001); + ('ROLE_ADMIN', 100001), + ('ROLE_USER', 100001); INSERT INTO meals (date_time, description, calories, user_id) VALUES ('2015-05-30 10:00:00', 'Завтрак', 500, 100000), diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index 086619eec978..a5a2b0b712f3 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -15,7 +15,7 @@ public class UserTestData { public static final int ADMIN_ID = START_SEQ + 1; public static final User USER = new User(USER_ID, "User", "user@yandex.ru", "password", Role.ROLE_USER); - public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ROLE_ADMIN); + public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ROLE_ADMIN, Role.ROLE_USER); public static User getNew() { return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.ROLE_USER)); @@ -25,11 +25,12 @@ public static User getUpdated() { User updated = new User(USER); updated.setName("UpdatedName"); updated.setCaloriesPerDay(330); + updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN)); return updated; } public static void assertMatch(User actual, User expected) { - assertThat(actual).isEqualToIgnoringGivenFields(expected, "registered", "roles", "meals"); + assertThat(actual).isEqualToIgnoringGivenFields(expected, "registered", "meals"); } public static void assertMatch(Iterable actual, User... expected) { @@ -37,6 +38,6 @@ public static void assertMatch(Iterable actual, User... expected) { } public static void assertMatch(Iterable actual, Iterable expected) { - assertThat(actual).usingElementComparatorIgnoringFields("registered", "roles", "meals").isEqualTo(expected); + assertThat(actual).usingElementComparatorIgnoringFields("registered", "meals").isEqualTo(expected); } } diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 10e367113caa..5a3548e68f9a 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -54,8 +54,8 @@ public void deletedNotFound() throws Exception { @Test public void get() throws Exception { - User user = service.get(USER_ID); - assertMatch(user, USER); + User user = service.get(ADMIN_ID); + assertMatch(user, ADMIN); } @Test(expected = NotFoundException.class) @@ -65,8 +65,8 @@ public void getNotFound() throws Exception { @Test public void getByEmail() throws Exception { - User user = service.getByEmail("user@yandex.ru"); - assertMatch(user, USER); + User user = service.getByEmail("admin@gmail.com"); + assertMatch(user, ADMIN); } @Test diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java index ca48cdcdfd04..9439fcffa96d 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -14,9 +14,9 @@ public class DataJpaUserServiceTest extends AbstractJpaUserServiceTest { @Test public void getWithMeals() throws Exception { - User user = service.getWithMeals(USER_ID); - assertMatch(user, USER); - MealTestData.assertMatch(user.getMeals(), MealTestData.MEALS); + User admin = service.getWithMeals(ADMIN_ID); + assertMatch(admin, ADMIN); + MealTestData.assertMatch(admin.getMeals(), MealTestData.ADMIN_MEAL2, MealTestData.ADMIN_MEAL1); } @Test(expected = NotFoundException.class) From d32c9a38d46f5641e64a449b47ec0d5d7eaef5eb Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:20:03 +0300 Subject: [PATCH 88/97] Applied patch - 7_05_fix_hint_graph --- .../topjava/repository/datajpa/CrudUserRepository.java | 3 ++- .../javawebinar/topjava/repository/jpa/JpaUserRepository.java | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java index 806884a9c90b..692e50c87c77 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -17,7 +17,8 @@ public interface CrudUserRepository extends JpaRepository { User getByEmail(String email); - @EntityGraph(attributePaths = {"meals", "roles"}) + // 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/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java index 9ade13354ac1..26c16f7df718 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -1,5 +1,6 @@ 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; @@ -61,6 +62,7 @@ public boolean delete(int id) { 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); } From db5f42e4a449194ab62ef7d4bad5c707ac829f07 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:24:09 +0300 Subject: [PATCH 89/97] Applied patch - 7_06_HW6_optional_jdbc --- .../repository/jdbc/JdbcMealRepository.java | 4 ++ .../repository/jdbc/JdbcUserRepository.java | 60 ++++++++++++++++--- src/main/resources/spring/spring-db.xml | 9 ++- .../service/AbstractUserServiceTest.java | 4 +- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index afe75a982514..53d824078159 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -8,6 +8,7 @@ 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; @@ -15,6 +16,7 @@ import java.util.List; @Repository +@Transactional(readOnly = true) public class JdbcMealRepository implements MealRepository { private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); @@ -35,6 +37,7 @@ public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate } @Override + @Transactional public Meal save(Meal meal, int userId) { MapSqlParameterSource map = new MapSqlParameterSource() .addValue("id", meal.getId()) @@ -59,6 +62,7 @@ public Meal save(Meal meal, int userId) { } @Override + @Transactional public boolean delete(int id, int userId) { return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0; } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java index 412bfbebc5df..43d654269536 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -8,12 +8,16 @@ 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.List; +import java.util.*; @Repository +@Transactional(readOnly = true) public class JdbcUserRepository implements UserRepository { private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class); @@ -35,21 +39,31 @@ public JdbcUserRepository(JdbcTemplate jdbcTemplate, 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()); - } 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; + 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; } @@ -57,18 +71,48 @@ public boolean delete(int id) { @Override public User get(int id) { List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id); - return DataAccessUtils.singleResult(users); + 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 DataAccessUtils.singleResult(users); + return setRoles(DataAccessUtils.singleResult(users)); } @Override public List getAll() { - return jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + 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.queryForList("SELECT role FROM user_roles WHERE user_id=?", Role.class, u.getId()); + u.setRoles(roles); + } + return u; } } diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index d26ec2d0a361..1b1f450ea634 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -20,6 +20,8 @@ + + @@ -66,6 +68,11 @@ + + + + @@ -101,8 +108,6 @@
    - - diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index 5a3548e68f9a..7fadd7e6d5c5 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -29,7 +29,7 @@ public void setUp() throws Exception { @Test public void create() throws Exception { User newUser = getNew(); - User created = service.create(newUser); + User created = service.create(new User(newUser)); Integer newId = created.getId(); newUser.setId(newId); assertMatch(created, newUser); @@ -72,7 +72,7 @@ public void getByEmail() throws Exception { @Test public void update() throws Exception { User updated = getUpdated(); - service.update(updated); + service.update(new User(updated)); assertMatch(service.get(USER_ID), updated); } From cacb3bc421a4587ce35e130d579cecf58873d522 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:25:24 +0300 Subject: [PATCH 90/97] Applied patch - 7_07_controller_test --- pom.xml | 14 ++++ .../topjava/AllActiveProfileResolver.java | 12 ++++ .../topjava/web/AbstractControllerTest.java | 65 +++++++++++++++++++ .../topjava/web/RootControllerTest.java | 29 +++++++++ 4 files changed, 120 insertions(+) create mode 100644 src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java create mode 100644 src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java create mode 100644 src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java diff --git a/pom.xml b/pom.xml index 5fefb5eed804..d3fedbf12494 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ 42.2.8 4.12 + 1.3 5.4.7.Final @@ -207,7 +208,20 @@ junit ${junit.version} test + + + hamcrest-core + org.hamcrest + + + + org.hamcrest + hamcrest-all + ${hamcrest.version} + test + + org.springframework spring-test 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/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java new file mode 100644 index 000000000000..2476368f1d35 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -0,0 +1,65 @@ +package ru.javawebinar.topjava.web; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +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 javax.annotation.PostConstruct; + +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-mvc.xml", + "classpath:spring/spring-db.xml" +}) +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.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 + private WebApplicationContext webApplicationContext; + + @PostConstruct + private void postConstruct() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .addFilter(CHARACTER_ENCODING_FILTER) + .build(); + } + + @Before + public void setUp() { + cacheManager.getCache("users").clear(); + if (jpaUtil != null) { + jpaUtil.clear2ndLevelHibernateCache(); + } + } +} 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..e8a1ae7d4ed7 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.web; + +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +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.UserTestData.USER; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class RootControllerTest extends AbstractControllerTest { + + @Test + public void getUsers() throws Exception { + mockMvc.perform(get("/users")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("users")) + .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) + .andExpect(model().attribute("users", hasSize(2))) + .andExpect(model().attribute("users", hasItem( + allOf( + hasProperty("id", is(START_SEQ)), + hasProperty("name", is(USER.getName())) + ) + ))); + } +} \ No newline at end of file From 1c95c4711984ab71afa01940325a6aa339ed6cc3 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:26:25 +0300 Subject: [PATCH 91/97] Applied patch - 7_08_JUnit5 --- pom.xml | 16 ++---- .../javawebinar/topjava/TimingExtension.java | 36 ++++++++++++ .../ru/javawebinar/topjava/TimingRules.java | 42 -------------- .../service/AbstractJpaUserServiceTest.java | 10 ++-- .../service/AbstractMealServiceTest.java | 55 ++++++++++--------- .../topjava/service/AbstractServiceTest.java | 51 +++++++---------- .../service/AbstractUserServiceTest.java | 47 +++++++++------- .../datajpa/DataJpaMealServiceTest.java | 14 +++-- .../datajpa/DataJpaUserServiceTest.java | 14 +++-- .../service/jdbc/JdbcMealServiceTest.java | 2 +- .../service/jdbc/JdbcUserServiceTest.java | 2 +- .../service/jpa/JpaMealServiceTest.java | 2 +- .../service/jpa/JpaUserServiceTest.java | 2 +- .../topjava/web/AbstractControllerTest.java | 17 +++--- .../topjava/web/RootControllerTest.java | 4 +- ...InMemoryAdminRestControllerSpringTest.java | 30 +++++----- .../user/InMemoryAdminRestControllerTest.java | 35 ++++++------ 17 files changed, 181 insertions(+), 198 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/TimingExtension.java delete mode 100644 src/test/java/ru/javawebinar/topjava/TimingRules.java diff --git a/pom.xml b/pom.xml index d3fedbf12494..6c38b5d13338 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 42.2.8 - 4.12 + 5.5.2 1.3 @@ -53,6 +53,7 @@ + org.apache.maven.plugins maven-surefire-plugin 2.22.2 @@ -204,17 +205,12 @@ - junit - junit - ${junit.version} + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} test - - - hamcrest-core - org.hamcrest - - + org.hamcrest hamcrest-all 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/TimingRules.java b/src/test/java/ru/javawebinar/topjava/TimingRules.java deleted file mode 100644 index 5a13a6f84fed..000000000000 --- a/src/test/java/ru/javawebinar/topjava/TimingRules.java +++ /dev/null @@ -1,42 +0,0 @@ -package ru.javawebinar.topjava; - -import org.junit.rules.ExternalResource; -import org.junit.rules.Stopwatch; -import org.junit.runner.Description; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.TimeUnit; - -public class TimingRules { - private static final Logger log = LoggerFactory.getLogger("result"); - - private static StringBuilder results = new StringBuilder(); - - // https://dzone.com/articles/applying-new-jdk-11-string-methods - private static String DELIM = "-".repeat(103); - - // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev - public static final Stopwatch STOPWATCH = new Stopwatch() { - @Override - protected void finished(long nanos, Description description) { - String result = String.format("%-95s %7d", description.getDisplayName(), TimeUnit.NANOSECONDS.toMillis(nanos)); - results.append(result).append('\n'); - log.info(result + " ms\n"); - } - }; - - public static final ExternalResource SUMMARY = new ExternalResource() { - @Override - protected void before() throws Throwable { - results.setLength(0); - } - - @Override - protected void after() { - log.info("\n" + DELIM + - "\nTest Duration, ms" + - "\n" + DELIM + "\n" + results + DELIM + "\n"); - } - }; -} diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java index 753e2b10f396..46ae26af8994 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java @@ -1,7 +1,7 @@ package ru.javawebinar.topjava.service; -import org.junit.Before; -import org.junit.Test; +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; @@ -16,15 +16,15 @@ abstract public class AbstractJpaUserServiceTest extends AbstractUserServiceTest @Autowired private JpaUtil jpaUtil; - @Before + @BeforeEach @Override - public void setUp() throws Exception { + void setUp() throws Exception { super.setUp(); jpaUtil.clear2ndLevelHibernateCache(); } @Test - public void createWithException() throws Exception { + void createWithException() throws Exception { validateRootCause(() -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.ROLE_USER)), ConstraintViolationException.class); validateRootCause(() -> service.create(new User(null, "User", " ", "password", Role.ROLE_USER)), ConstraintViolationException.class); validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.ROLE_USER)), ConstraintViolationException.class); diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java index 1e2e82ddd6a7..ac0ba85b1bd7 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -1,7 +1,7 @@ package ru.javawebinar.topjava.service; -import org.junit.Assume; -import org.junit.Test; +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.NotFoundException; @@ -11,6 +11,8 @@ import java.time.Month; import static java.time.LocalDateTime.of; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; import static ru.javawebinar.topjava.UserTestData.USER_ID; @@ -21,26 +23,26 @@ public abstract class AbstractMealServiceTest extends AbstractServiceTest { protected MealService service; @Test - public void delete() throws Exception { + void delete() throws Exception { service.delete(MEAL1_ID, USER_ID); - thrown.expect(NotFoundException.class); - service.get(MEAL1_ID, USER_ID); + assertThrows(NotFoundException.class, () -> + service.get(MEAL1_ID, USER_ID)); } @Test - public void deleteNotFound() throws Exception { - thrown.expect(NotFoundException.class); - service.delete(1, USER_ID); + void deleteNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.delete(1, USER_ID)); } @Test - public void deleteNotOwn() throws Exception { - thrown.expect(NotFoundException.class); - service.delete(MEAL1_ID, ADMIN_ID); + void deleteNotOwn() throws Exception { + assertThrows(NotFoundException.class, () -> + service.delete(MEAL1_ID, ADMIN_ID)); } @Test - public void create() throws Exception { + void create() throws Exception { Meal newMeal = getNew(); Meal created = service.create(newMeal, USER_ID); Integer newId = created.getId(); @@ -50,44 +52,43 @@ public void create() throws Exception { } @Test - public void get() throws Exception { + void get() throws Exception { Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); assertMatch(actual, ADMIN_MEAL1); } @Test - public void getNotFound() throws Exception { - thrown.expect(NotFoundException.class); - service.get(1, ADMIN_ID); + void getNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.get(1, ADMIN_ID)); } @Test - public void getNotOwn() throws Exception { - thrown.expect(NotFoundException.class); - service.get(MEAL1_ID, ADMIN_ID); + void getNotOwn() throws Exception { + assertThrows(NotFoundException.class, () -> + service.get(MEAL1_ID, ADMIN_ID)); } @Test - public void update() throws Exception { + void update() throws Exception { Meal updated = getUpdated(); service.update(updated, USER_ID); assertMatch(service.get(MEAL1_ID, USER_ID), updated); } @Test - public void updateNotFound() throws Exception { - thrown.expect(NotFoundException.class); - thrown.expectMessage("Not found entity with id=" + MEAL1_ID); - service.update(MEAL1, ADMIN_ID); + void updateNotFound() throws Exception { + NotFoundException e = assertThrows(NotFoundException.class, () -> service.update(MEAL1, ADMIN_ID)); + assertEquals(e.getMessage(), "Not found entity with id=" + MEAL1_ID); } @Test - public void getAll() throws Exception { + void getAll() throws Exception { assertMatch(service.getAll(USER_ID), MEALS); } @Test - public void getBetween() throws Exception { + void getBetween() throws Exception { assertMatch(service.getBetweenDates( LocalDate.of(2015, Month.MAY, 30), LocalDate.of(2015, Month.MAY, 30), USER_ID), MEAL3, MEAL2, MEAL1); @@ -100,7 +101,7 @@ public void getBetweenWithNullDates() throws Exception { @Test public void createWithException() throws Exception { - Assume.assumeTrue(isJpaBased()); + 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); diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index 97ecc308f00d..f166b03246dc 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -1,58 +1,45 @@ package ru.javawebinar.topjava.service; -import org.junit.Assert; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.rules.ExpectedException; -import org.junit.rules.ExternalResource; -import org.junit.rules.Stopwatch; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.Profiles; -import ru.javawebinar.topjava.TimingRules; +import ru.javawebinar.topjava.TimingExtension; -import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause; -@ContextConfiguration({ +@SpringJUnitConfig(locations = { "classpath:spring/spring-app.xml", "classpath:spring/spring-db.xml" }) -@RunWith(SpringRunner.class) -@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +//@ExtendWith(SpringExtension.class) @ActiveProfiles(resolver = ActiveDbProfileResolver.class) -abstract public class AbstractServiceTest { - @ClassRule - public static ExternalResource summary = TimingRules.SUMMARY; - - @Rule - public Stopwatch stopwatch = TimingRules.STOPWATCH; +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ExtendWith(TimingExtension.class) +abstract class AbstractServiceTest { @Autowired - public Environment env; - - @Rule - public ExpectedException thrown = ExpectedException.none(); + private Environment env; - public boolean isJpaBased() { + 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 - public void validateRootCause(Runnable runnable, Class exceptionClass) { - try { - runnable.run(); - Assert.fail("Expected " + exceptionClass.getName()); - } catch (Exception e) { - Assert.assertThat(getRootCause(e), instanceOf(exceptionClass)); - } + 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 index 7fadd7e6d5c5..ed18de5fe834 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -1,7 +1,7 @@ package ru.javawebinar.topjava.service; -import org.junit.Before; -import org.junit.Test; +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; @@ -11,6 +11,7 @@ import java.util.List; +import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.javawebinar.topjava.UserTestData.*; public abstract class AbstractUserServiceTest extends AbstractServiceTest { @@ -21,13 +22,13 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { @Autowired private CacheManager cacheManager; - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp() throws Exception { cacheManager.getCache("users").clear(); } @Test - public void create() throws Exception { + void create() throws Exception { User newUser = getNew(); User created = service.create(new User(newUser)); Integer newId = created.getId(); @@ -36,48 +37,52 @@ public void create() throws Exception { assertMatch(service.get(newId), newUser); } - @Test(expected = DataAccessException.class) - public void duplicateMailCreate() throws Exception { - service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.ROLE_USER)); + @Test + void duplicateMailCreate() throws Exception { + assertThrows(DataAccessException.class, () -> + service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.ROLE_USER))); } - @Test(expected = NotFoundException.class) - public void delete() throws Exception { + @Test + void delete() throws Exception { service.delete(USER_ID); - service.get(USER_ID); + assertThrows(NotFoundException.class, () -> + service.delete(USER_ID)); } - @Test(expected = NotFoundException.class) - public void deletedNotFound() throws Exception { - service.delete(1); + @Test + void deletedNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.delete(1)); } @Test - public void get() throws Exception { + void get() throws Exception { User user = service.get(ADMIN_ID); assertMatch(user, ADMIN); } - @Test(expected = NotFoundException.class) - public void getNotFound() throws Exception { - service.get(1); + @Test + void getNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.get(1)); } @Test - public void getByEmail() throws Exception { + void getByEmail() throws Exception { User user = service.getByEmail("admin@gmail.com"); assertMatch(user, ADMIN); } @Test - public void update() throws Exception { + void update() throws Exception { User updated = getUpdated(); service.update(new User(updated)); assertMatch(service.get(USER_ID), updated); } @Test - public void getAll() throws Exception { + void getAll() throws Exception { List all = service.getAll(); assertMatch(all, ADMIN, USER); } diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java index a1da4b23a08c..350cfadd4f0e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -1,27 +1,29 @@ package ru.javawebinar.topjava.service.datajpa; -import org.junit.Test; +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) -public class DataJpaMealServiceTest extends AbstractMealServiceTest { +class DataJpaMealServiceTest extends AbstractMealServiceTest { @Test - public void getWithUser() throws Exception { + void getWithUser() throws Exception { Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID); assertMatch(adminMeal, ADMIN_MEAL1); UserTestData.assertMatch(adminMeal.getUser(), UserTestData.ADMIN); } - @Test(expected = NotFoundException.class) - public void getWithUserNotFound() throws Exception { - service.getWithUser(1, ADMIN_ID); + @Test + void getWithUserNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> + service.getWithUser(1, 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 index 9439fcffa96d..2c4a584483c7 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -1,26 +1,28 @@ package ru.javawebinar.topjava.service.datajpa; -import org.junit.Test; +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) -public class DataJpaUserServiceTest extends AbstractJpaUserServiceTest { +class DataJpaUserServiceTest extends AbstractJpaUserServiceTest { @Test - public void getWithMeals() throws Exception { + void getWithMeals() throws Exception { User admin = service.getWithMeals(ADMIN_ID); assertMatch(admin, ADMIN); MealTestData.assertMatch(admin.getMeals(), MealTestData.ADMIN_MEAL2, MealTestData.ADMIN_MEAL1); } - @Test(expected = NotFoundException.class) - public void getWithMealsNotFound() throws Exception { - service.getWithMeals(1); + @Test + void getWithMealsNotFound() 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 index 9ff4ae615b2b..aef588264f71 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -6,5 +6,5 @@ import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) -public class JdbcMealServiceTest extends AbstractMealServiceTest { +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 index 419f68ed1098..62ca7668cf67 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -6,5 +6,5 @@ import static ru.javawebinar.topjava.Profiles.JDBC; @ActiveProfiles(JDBC) -public class JdbcUserServiceTest extends AbstractUserServiceTest { +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 index 70e7bf865421..aaf5dcda960e 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java @@ -6,5 +6,5 @@ import static ru.javawebinar.topjava.Profiles.JPA; @ActiveProfiles(JPA) -public class JpaMealServiceTest extends AbstractMealServiceTest { +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 index ffb1fea03c22..d903cf67a6dd 100644 --- a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java @@ -6,5 +6,5 @@ import static ru.javawebinar.topjava.Profiles.JPA; @ActiveProfiles(JPA) -public class JpaUserServiceTest extends AbstractJpaUserServiceTest { +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 index 2476368f1d35..d8a71cb2cd17 100644 --- a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -1,13 +1,10 @@ package ru.javawebinar.topjava.web; -import org.junit.Before; -import org.junit.runner.RunWith; +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.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; @@ -18,13 +15,13 @@ import javax.annotation.PostConstruct; -@ContextConfiguration({ +@SpringJUnitWebConfig(locations = { "classpath:spring/spring-app.xml", "classpath:spring/spring-mvc.xml", "classpath:spring/spring-db.xml" }) -@WebAppConfiguration -@RunWith(SpringJUnit4ClassRunner.class) +//@WebAppConfiguration +//@ExtendWith(SpringExtension.class) @Transactional @ActiveProfiles(resolver = AllActiveProfileResolver.class) abstract public class AbstractControllerTest { @@ -55,8 +52,8 @@ private void postConstruct() { .build(); } - @Before - public void setUp() { + @BeforeEach + void setUp() { cacheManager.getCache("users").clear(); if (jpaUtil != null) { jpaUtil.clear2ndLevelHibernateCache(); diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java index e8a1ae7d4ed7..efdd1640afd7 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -1,6 +1,6 @@ package ru.javawebinar.topjava.web; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -9,7 +9,7 @@ import static ru.javawebinar.topjava.UserTestData.USER; import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; -public class RootControllerTest extends AbstractControllerTest { +class RootControllerTest extends AbstractControllerTest { @Test public void getUsers() throws Exception { diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java index 872a3b1736c9..850ec575ead2 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -1,19 +1,17 @@ package ru.javawebinar.topjava.web.user; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import ru.javawebinar.topjava.repository.inmemory.InMemoryUserRepository; import ru.javawebinar.topjava.util.exception.NotFoundException; +import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.javawebinar.topjava.UserTestData.USER_ID; -@ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) -@RunWith(SpringRunner.class) -public class InMemoryAdminRestControllerSpringTest { +@SpringJUnitConfig(locations = {"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) +class InMemoryAdminRestControllerSpringTest { @Autowired private AdminRestController controller; @@ -21,19 +19,19 @@ public class InMemoryAdminRestControllerSpringTest { @Autowired private InMemoryUserRepository repository; - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp() throws Exception { repository.init(); } - @Test(expected = NotFoundException.class) - public void delete() throws Exception { + @Test + void delete() throws Exception { controller.delete(USER_ID); - controller.get(USER_ID); + assertThrows(NotFoundException.class, () -> controller.get(USER_ID)); } - @Test(expected = NotFoundException.class) - public void deleteNotFound() throws Exception { - controller.delete(10); + @Test + void deleteNotFound() 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 index 67c295c02172..d41fca9e1f03 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -1,9 +1,9 @@ package ru.javawebinar.topjava.web.user; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ConfigurableApplicationContext; @@ -13,43 +13,44 @@ import java.util.Arrays; +import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.javawebinar.topjava.UserTestData.USER_ID; -public class InMemoryAdminRestControllerTest { +class InMemoryAdminRestControllerTest { private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); private static ConfigurableApplicationContext appCtx; private static AdminRestController controller; - @BeforeClass - public static void beforeClass() { + @BeforeAll + static void beforeClass() { appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/inmemory.xml"); log.info("\n{}\n", Arrays.toString(appCtx.getBeanDefinitionNames())); controller = appCtx.getBean(AdminRestController.class); } - @AfterClass - public static void afterClass() { + @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(); } - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp() throws Exception { // re-initialize InMemoryUserRepository repository = appCtx.getBean(InMemoryUserRepository.class); repository.init(); } - @Test(expected = NotFoundException.class) - public void delete() throws Exception { + @Test + void delete() throws Exception { controller.delete(USER_ID); - controller.get(USER_ID); + assertThrows(NotFoundException.class, () -> controller.get(USER_ID)); } - @Test(expected = NotFoundException.class) - public void deleteNotFound() throws Exception { - controller.delete(10); + @Test + void deleteNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> controller.delete(10)); } } \ No newline at end of file From 06ec16fab2f82c41ac8c3e3118b58f75806ef16a Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:29:43 +0300 Subject: [PATCH 92/97] Applied patch - 7_09_rest_controller --- .../topjava/web/user/AdminRestController.java | 39 +++++++++++++------ .../web/user/ProfileRestController.java | 15 +++++-- .../ru/javawebinar/topjava/SpringMain.java | 4 +- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index b37a8ed6c8a5..59cf779d98c5 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -1,40 +1,57 @@ package ru.javawebinar.topjava.web.user; -import org.springframework.stereotype.Controller; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import ru.javawebinar.topjava.model.User; +import java.net.URI; import java.util.List; -@Controller +@RestController +@RequestMapping(value = AdminRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) public class AdminRestController extends AbstractUserController { - @Override + static final String REST_URL = "/rest/admin/users"; + + @GetMapping public List getAll() { return super.getAll(); } @Override - public User get(int id) { + @GetMapping("/{id}") + public User get(@PathVariable int id) { return super.get(id); } - @Override - public User create(User user) { - return super.create(user); + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@RequestBody User user) { + User created = super.create(user); + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + return ResponseEntity.created(uriOfNewResource).body(created); } @Override - public void delete(int id) { + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { super.delete(id); } @Override - public void update(User user, int id) { + @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void update(@RequestBody User user, @PathVariable int id) { super.update(user, id); } - @Override - public User getByMail(String email) { + @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/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index 7d3702c31c46..07ea30e2b27b 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -1,22 +1,31 @@ package ru.javawebinar.topjava.web.user; -import org.springframework.stereotype.Controller; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.model.User; import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; -@Controller +@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() { return super.get(authUserId()); } + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) public void delete() { super.delete(authUserId()); } - public void update(User user) { + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void update(@RequestBody User user) { super.update(user, authUserId()); } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java index bd02c9b01821..3e4d65389b0b 100644 --- a/src/test/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -1,8 +1,6 @@ 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; @@ -23,7 +21,7 @@ public static void main(String[] args) { System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); - adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ROLE_ADMIN)); + adminUserController.getAll(); System.out.println(); MealRestController mealController = appCtx.getBean(MealRestController.class); From 91f8abfb5f734a8bcce7e27b402275ec225219cb Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:30:05 +0300 Subject: [PATCH 93/97] Applied patch - 7_10_rest_test_jackson --- pom.xml | 8 ++++++ .../topjava/model/AbstractBaseEntity.java | 5 ++++ .../ru/javawebinar/topjava/model/User.java | 2 ++ .../web/user/AdminRestControllerTest.java | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java diff --git a/pom.xml b/pom.xml index 6c38b5d13338..9cd081ec2f8a 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ 5.2.0.RELEASE 2.2.0.RELEASE + 2.10.1 9.0.27 @@ -203,6 +204,13 @@ spring-webmvc + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-json.version} + + org.junit.jupiter diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index f8f60b1f72d2..16346ab9e140 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,13 +1,18 @@ package ru.javawebinar.topjava.model; +import com.fasterxml.jackson.annotation.JsonAutoDetect; import org.hibernate.Hibernate; import org.springframework.data.domain.Persistable; import javax.persistence.*; +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; + @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 Persistable { public static final int START_SEQ = 100000; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 3cf7408b8dea..47fe153d18f8 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @@ -62,6 +63,7 @@ public class User extends AbstractNamedEntity { @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") + @JsonIgnore protected List meals; public User() { 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..2de0360822cc --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -0,0 +1,25 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.web.AbstractControllerTest; + +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.UserTestData.ADMIN_ID; + +class AdminRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = AdminRestController.REST_URL + '/'; + + @Test + void get() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID)) + .andExpect(status().isOk()) + .andDo(print()) + // https://jira.spring.io/browse/SPR-14472 + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + } +} \ No newline at end of file From 09c9a37e582fa2a0399300c92e71b0370aa5b784 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:30:50 +0300 Subject: [PATCH 94/97] Applied patch - 7_11_jackson_object_mapper --- pom.xml | 10 ++++++ .../topjava/model/AbstractBaseEntity.java | 6 +--- .../ru/javawebinar/topjava/model/User.java | 3 +- .../topjava/web/json/JacksonObjectMapper.java | 31 +++++++++++++++++++ src/main/resources/spring/spring-mvc.xml | 11 ++++++- 5 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java diff --git a/pom.xml b/pom.xml index 9cd081ec2f8a..92125e92242e 100644 --- a/pom.xml +++ b/pom.xml @@ -210,6 +210,16 @@ 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} + diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index 16346ab9e140..554abacc4da6 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,18 +1,14 @@ package ru.javawebinar.topjava.model; -import com.fasterxml.jackson.annotation.JsonAutoDetect; import org.hibernate.Hibernate; import org.springframework.data.domain.Persistable; import javax.persistence.*; -import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; -import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; - @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) +//@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE) public abstract class AbstractBaseEntity implements Persistable { public static final int START_SEQ = 100000; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 47fe153d18f8..67b32d66d7fc 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,6 +1,5 @@ package ru.javawebinar.topjava.model; -import com.fasterxml.jackson.annotation.JsonIgnore; import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @@ -63,7 +62,7 @@ public class User extends AbstractNamedEntity { @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") - @JsonIgnore +// @JsonIgnore protected List meals; public User() { 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..d91b1007b012 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java @@ -0,0 +1,31 @@ +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 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); + } +} \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index ce12ddcc64d5..50f7bf45c4c9 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -5,7 +5,16 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> - + + + + + + + + + + From dc1f6c85356a70709c0743b9d6674d5aa82fcd26 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:31:08 +0300 Subject: [PATCH 95/97] Applied patch - 7_12_json_assert_tests --- pom.xml | 7 +++ .../topjava/web/json/JacksonObjectMapper.java | 6 +++ .../topjava/web/json/JsonUtil.java | 47 +++++++++++++++++++ src/main/resources/spring/spring-mvc.xml | 7 ++- .../topjava/web/json/JsonUtilTest.java | 27 +++++++++++ .../web/user/AdminRestControllerTest.java | 5 +- 6 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java create mode 100644 src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java diff --git a/pom.xml b/pom.xml index 92125e92242e..4fecfbeb7b4c 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ 5.5.2 1.3 + 1.5.0 5.4.7.Final @@ -235,6 +236,12 @@ ${hamcrest.version} test + + org.skyscreamer + jsonassert + ${jsonassert.version} + test + org.springframework diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java index d91b1007b012..8237df93bffe 100644 --- a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java +++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java @@ -18,6 +18,8 @@ */ public class JacksonObjectMapper extends ObjectMapper { + private static final ObjectMapper MAPPER = new JacksonObjectMapper(); + private JacksonObjectMapper() { registerModule(new Hibernate5Module()); @@ -28,4 +30,8 @@ private JacksonObjectMapper() { 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..b931bbe045a9 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java @@ -0,0 +1,47 @@ +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 writeIgnoreProps(T obj, String... ignoreProps) { + Map map = getMapper().convertValue(obj, new TypeReference>() {}); + for (String prop : ignoreProps) { + map.remove(prop); + } + return writeValue(map); + } +} \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 50f7bf45c4c9..fa99f089f8c4 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -5,17 +5,16 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> + + - - - + - 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..a29d7f71e53c --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java @@ -0,0 +1,27 @@ +package ru.javawebinar.topjava.web.json; + +import org.junit.jupiter.api.Test; +import ru.javawebinar.topjava.model.Meal; + +import java.util.List; + +import static ru.javawebinar.topjava.MealTestData.*; + +class JsonUtilTest { + + @Test + void readWriteValue() 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 readWriteValues() throws Exception { + String json = JsonUtil.writeValue(MEALS); + System.out.println(json); + List meals = JsonUtil.readValues(json, Meal.class); + assertMatch(meals, MEALS); + } +} \ 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 index 2de0360822cc..e1672f89a736 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -8,7 +8,9 @@ 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.UserTestData.ADMIN; import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.web.json.JsonUtil.writeIgnoreProps; class AdminRestControllerTest extends AbstractControllerTest { @@ -20,6 +22,7 @@ void get() throws Exception { .andExpect(status().isOk()) .andDo(print()) // https://jira.spring.io/browse/SPR-14472 - .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)); + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(content().json(writeIgnoreProps(ADMIN, "registered"))); } } \ No newline at end of file From b9ac014a189f7be460cd96ab7e561b846281d26b Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:31:26 +0300 Subject: [PATCH 96/97] Applied patch - 7_13_tests_refactoring --- pom.xml | 9 +-- .../topjava/web/json/JsonUtil.java | 10 --- .../java/ru/javawebinar/topjava/TestUtil.java | 26 +++++++ .../ru/javawebinar/topjava/UserTestData.java | 11 +++ .../topjava/web/RootControllerTest.java | 23 ++++--- .../web/user/AdminRestControllerTest.java | 68 +++++++++++++++++-- .../web/user/ProfileRestControllerTest.java | 49 +++++++++++++ 7 files changed, 164 insertions(+), 32 deletions(-) create mode 100644 src/test/java/ru/javawebinar/topjava/TestUtil.java create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java diff --git a/pom.xml b/pom.xml index 4fecfbeb7b4c..9e8299060ee2 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,6 @@ 5.5.2 1.3 - 1.5.0 5.4.7.Final @@ -232,16 +231,10 @@ org.hamcrest - hamcrest-all + hamcrest-core ${hamcrest.version} test - - org.skyscreamer - jsonassert - ${jsonassert.version} - test - org.springframework diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java index b931bbe045a9..fda04590d618 100644 --- a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java @@ -1,12 +1,10 @@ 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; @@ -36,12 +34,4 @@ public static String writeValue(T obj) { throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e); } } - - public static String writeIgnoreProps(T obj, String... ignoreProps) { - Map map = getMapper().convertValue(obj, new TypeReference>() {}); - for (String prop : ignoreProps) { - map.remove(prop); - } - return writeValue(map); - } } \ No newline at end of file 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..3c1b633030bd --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java @@ -0,0 +1,26 @@ +package ru.javawebinar.topjava; + +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +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); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index a5a2b0b712f3..cb6785faa2a2 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava; +import org.springframework.test.web.servlet.ResultMatcher; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; @@ -8,6 +9,8 @@ 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 { @@ -40,4 +43,12 @@ public static void assertMatch(Iterable actual, User... expected) { public static void assertMatch(Iterable actual, Iterable expected) { assertThat(actual).usingElementComparatorIgnoringFields("registered", "meals").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); + } } diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java index efdd1640afd7..b5d38b2b532b 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -1,13 +1,15 @@ package ru.javawebinar.topjava.web; +import org.assertj.core.matcher.AssertionMatcher; import org.junit.jupiter.api.Test; +import ru.javawebinar.topjava.model.User; + +import java.util.List; -import static org.hamcrest.Matchers.*; 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.UserTestData.USER; -import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; +import static ru.javawebinar.topjava.UserTestData.*; class RootControllerTest extends AbstractControllerTest { @@ -18,12 +20,13 @@ public void getUsers() throws Exception { .andExpect(status().isOk()) .andExpect(view().name("users")) .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) - .andExpect(model().attribute("users", hasSize(2))) - .andExpect(model().attribute("users", hasItem( - allOf( - hasProperty("id", is(START_SEQ)), - hasProperty("name", is(USER.getName())) - ) - ))); + .andExpect(model().attribute("users", + new AssertionMatcher>() { + @Override + public void assertion(List actual) throws AssertionError { + assertMatch(actual, ADMIN, USER); + } + } + )); } } \ 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 index e1672f89a736..c40282dd40e7 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -1,21 +1,31 @@ package ru.javawebinar.topjava.web.user; 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.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.util.exception.NotFoundException; import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; +import static org.junit.jupiter.api.Assertions.assertThrows; 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.UserTestData.ADMIN; -import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; -import static ru.javawebinar.topjava.web.json.JsonUtil.writeIgnoreProps; +import static ru.javawebinar.topjava.TestUtil.readFromJson; +import static ru.javawebinar.topjava.UserTestData.*; class AdminRestControllerTest extends AbstractControllerTest { private static final String REST_URL = AdminRestController.REST_URL + '/'; + @Autowired + private UserService userService; + @Test void get() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID)) @@ -23,6 +33,56 @@ void get() throws Exception { .andDo(print()) // https://jira.spring.io/browse/SPR-14472 .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(content().json(writeIgnoreProps(ADMIN, "registered"))); + .andExpect(contentJson(ADMIN)); + } + + @Test + void getByEmail() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get(REST_URL + "by?email=" + USER.getEmail())) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(contentJson(USER)); + } + + @Test + void delete() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.delete(REST_URL + USER_ID)) + .andDo(print()) + .andExpect(status().isNoContent()); + assertThrows(NotFoundException.class, () -> userService.get(USER_ID)); + } + + @Test + void update() throws Exception { + User updated = UserTestData.getUpdated(); + mockMvc.perform(MockMvcRequestBuilders.put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated))) + .andExpect(status().isNoContent()); + + assertMatch(userService.get(USER_ID), updated); + } + + @Test + void createWithLocation() throws Exception { + User newUser = UserTestData.getNew(); + ResultActions action = mockMvc.perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(newUser))) + .andExpect(status().isCreated()); + + User created = readFromJson(action, User.class); + Integer newId = created.getId(); + newUser.setId(newId); + assertMatch(created, newUser); + assertMatch(userService.get(newId), newUser); + } + + @Test + void getAll() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(contentJson(ADMIN, USER)); } } \ 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..589ff4f71427 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -0,0 +1,49 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +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.UserTestData.*; +import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; + +class ProfileRestControllerTest extends AbstractControllerTest { + + @Autowired + private UserService userService; + + @Test + void get() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(contentJson(USER)); + } + + @Test + void delete() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.delete(REST_URL)) + .andExpect(status().isNoContent()); + assertMatch(userService.getAll(), ADMIN); + } + + @Test + void update() throws Exception { + User updated = UserTestData.getUpdated(); + mockMvc.perform(MockMvcRequestBuilders.put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated))) + .andDo(print()) + .andExpect(status().isNoContent()); + + assertMatch(userService.get(USER_ID), updated); + } +} \ No newline at end of file From 843de969a6b18736243fbd4dd733770116f561f7 Mon Sep 17 00:00:00 2001 From: alkone Date: Thu, 28 Nov 2019 20:31:53 +0300 Subject: [PATCH 97/97] Applied patch - 7_14_soapui_utf8_converter --- config/Topjava-soapui-project.xml | 369 ++++++++++++++++++ .../web/user/ProfileRestController.java | 5 + src/main/resources/spring/spring-mvc.xml | 8 + 3 files changed, 382 insertions(+) create mode 100644 config/Topjava-soapui-project.xml diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml new file mode 100644 index 000000000000..65d20413ebb4 --- /dev/null +++ b/config/Topjava-soapui-project.xml @@ -0,0 +1,369 @@ + + + + + + + + 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 + + 200data200data + + <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 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index 07ea30e2b27b..eeaae2dfe739 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -28,4 +28,9 @@ public void delete() { public void update(@RequestBody User user) { super.update(user, authUserId()); } + + @GetMapping(value = "/text") + public String testUTF() { + return "Русский текст"; + } } \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index fa99f089f8c4..2be2c2d516cd 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -12,6 +12,14 @@ + + + + + + + +