diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 000000000000..311a8f4e0029 --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,5 @@ +--- +exclude_paths: + - 'src/main/webapp/**' + - '**.md' + - '**.sql' \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000000..2fda05fb5b33 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +# https://docs.travis-ci.com/user/languages/java/ +language: java +jdk: openjdk15 + +#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/README.md b/README.md index 228b708b7094..ffeea60a416c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/7cbbfc11cdca4502899c50db977bbfe3)](https://www.codacy.com/gh/Ar-Rafaella-Redis/topjava22/dashboard) +[![Build Status](https://api.travis-ci.com/Ar-Rafaella-Redis/topjava22.svg?branch=master)](https://travis-ci.com/Ar-Rafaella-Redis/topjava22) + 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. @@ -41,9 +44,16 @@ Java Enterprise Online Project - Git Overview - [Основы Git за 20 минут](https://www.youtube.com/watch?v=TMeZGvtQnT8) - [Git - для новичков](https://www.youtube.com/watch?list=PLY4rE9dstrJyTdVJpv7FibSaXB4BHPInb&v=PEKN8NtBDQ0) + - [Руководство по написанию комментариев в коммитах](https://techrocks.ru/2019/12/02/writing-good-commit-messages) ## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Работа с проектом (выполнять инструкции) -**ВНИМАНИЕ: выбирайте для проекта простой пусть без пробелов и русских букв, например (Windows) `c:\projects\topjava\`. Иначе впоследствии будут проблемы** +- **ВНИМАНИЕ: выбирайте для проекта простой пусть без пробелов и русских букв, например (Windows) `c:\projects\topjava\`. Иначе впоследствии будут проблемы** +- **Плагин уже Git Intergation не требуется и вкладку `Version control` в IDEA переименовали в `Git`** + +Для переключения режима отображения изменений из вкладки Commit в Git: Local Changes нужно переключить `Settings/Preferences | Version Control | Commit | Use non-modal commit interface` или в контекстном меню вкладки `Commit`: + +![image](https://user-images.githubusercontent.com/13649199/105491518-72d8f300-5cc7-11eb-8b79-c46382562deb.png) ![image](https://user-images.githubusercontent.com/13649199/105488663-05c35e80-5cc3-11eb-962e-30f403d623e8.png) + ### Патч [prepare_to_HW0.patch](https://drive.google.com/file/d/1LNPpu9OkuCpfpD8ZJHO-o0vwu49p2i5M) (скачать и положить в каталог вашего проекта) > Проект постоянно улучшается, поэтому видео иногда отличается от кода проекта. Изменения указываю после видео: @@ -51,7 +61,7 @@ Java Enterprise Online Project > - в `UserMeals/UserMealWithExcess` поля изменились на `private` > - обновил данные `UserMealsUtil.meals` и переименовал некоторые пременные, поля и методы > - добавил `UserMealWithExcess.toString()` и метод для выполнения _Optional домашнего задания_ - +> - метод фильтрации в `TimeUtil` переименовали в `isBetweenHalfOpen` (также изменилась логика сравнения - `startTime` включается в интервал) ## Инструкция по шагам (из видео): - Установить ПО (git, JDK8, IntelliJ IDEA, Maven) @@ -69,6 +79,9 @@ Java Enterprise Online Project - Выполнить задание и залить на GitHub (commit + push) - Переключиться в основную ветку проекта master. +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. [Тех.задание: библия или допускаются изменения. Полуоткрытый интервал.](https://drive.google.com/file/d/1BpTzjNFjS0TSekCyt_xvt6YoLvuw5KTZ/view) +- [Типы промежутков](https://ru.wikipedia.org/wiki/Промежуток_(математика)) + ## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW0 ``` Реализовать метод `UserMealsUtil.filteredByCycles` через циклы (`forEach`): @@ -82,7 +95,7 @@ Java Enterprise Online Project - Оцените Time complexity алгоритма. Если она больше O(N), например O(N*N) или N*log(N), сделайте O(N). ``` - Java 8 Date and Time API -- Алгоритмы и структуры данных для начинающих: сложность алгоритмов +- Алгоритмы и структуры данных для начинающих: сложность алгоритмов - [Головач: сложность алгоритмов в теме коллекций](https://www.youtube.com/watch?v=Ek9ijOiplNE&feature=youtu.be&t=778) - Time complexity - Временная сложность алгоритма @@ -98,27 +111,32 @@ Java Enterprise Online Project - Java 8: Lambda выражения - Java 8: Потоки - Pуководство по Java 8 Stream -- Java 8 Stream API в картинках и примерах +- [Полное руководство по Java 8 Stream API в картинках и примерах](https://annimon.com/article/2778) - [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.map vs forEach` - - без циклов по другим коллекциям - - решение должно быть рабочим в общем случае (не только при запуске main) + - без циклов по другим коллекциям/массивам (к ним также относим методы коллекций `addAll()/removeAll()`) + - решение должно быть рабочим в общем случае (работать в приложении с многими пользователями, не только при запуске main) - через Stream API за 1 проход по исходному списку `meals.streem()` - - нельзя использовать внешние коллекции, не являющиеся частью коллектора или 2 раза проходить по исходному списку (его копиям). - Т.е. в решении не должно быть 2 раза `meal.stream()` (в том числе неявно, в составных коллекторах) - - возможно дополнительные проходы по частям списка + - нельзя использовать внешние коллекции, не являющиеся частью коллектора + - нельзя 2 раза проходить по исходному списку (в том числе его отфильтрованной или преобразованной копии) + - возможно дополнительные проходы по частям списка, при этом превышение должно считаться один раз для всего подсписка. Те например нельзя разбить список на на 2 подсписка с четными и нечетными датами и затем их объединить, с подсчетом превышения для каждого элемента. + + +Ресурсы: +- [Java 8 Stream API, часть шестая: собственный коллектор](https://easyjava.ru/java/language/java-8-stream-api-chast-shestaya-sobstvennyj-kollektor) +- [Руководство по Java 8 Stream API: Collector](https://annimon.com/article/2778#collector) ### Замечания по использованию Stream API: - Когда встречаешь что-то непривычное, приходится перестраивать мозги. Например, переход с процедурного на ООП программирование дается непросто. Те, кто не знает шаблонов (и не хотят учить) также их встречают плохо. Хорошая новость в том, что если это принять и начать использовать, то начинаешь получать от этого удовольствие. И тут главное не впасть в другую крайность: @@ -133,7 +151,7 @@ Java Enterprise Online Project ## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания к HW0 - 1: Код проекта менять можно! Одна из распространенных ошибок как в тестовых заданиях на собеседовании, так и при работе на проекте, что ничего нельзя менять. Конечно при правках в рабочем проекте обязательно нужно проконсультироваться/проревьюироваться у авторов кода (находится по истории VCS) -- 2: Наследовать `UserMealWithExcess` от `UserMeal` я не буду, т.к. это разные сущности: Transfer Object и Entity. Мы будет их проходить на 2м уроке. +- 2: Наследовать `UserMealWithExcess` от `UserMeal` нельзя, т.к. это разные сущности: Transfer Object и Entity. Мы будет их проходить на 2м уроке. Это относится и к зависимости. - 3: Правильная реализация должна быть простой и красивой, можно сделать 2-мя способами: через стримы и через циклы. Сложность должна быть O(N), т.е. без вложенных стримов и циклов. - 4: При реализации через циклы посмотрите в `Map` на методы `getOrDefault` или `merge` - 5: **При реализации через `Stream` заменяйте `forEach` оператором `stream.map(..)`** @@ -144,13 +162,13 @@ Java Enterprise Online Project - 10: `System.out.println` нельзя делать нигде, кроме как в `main`. Позже введем логирование. - 11: Результаты, возвращаемые `UserMealsUtil.filteredByStreams` мы будем использовать [в нашем приложении](http://topjava.herokuapp.com/) для фильтрации по времени и отображения еды правильным цветом. - 12: Обращайте внимание на комментарии к вашим коммитам в git. Они должны быть короткие и информативные (лучше на english) -- 13: Не полагайтесь в решении на то, что список будет подаваться отсортированным. Такого условия нет. +- 13: Не полагайтесь в решении на то, что список еды будет подаваться отсортированным. Такого условия нет. ----- ## [Пример 7-го занятия онлайн стажировки, несколько видео открыто](https://github.com/JavaOPs/topjava/blob/master/doc/lesson07.md) ### Полезные ресурсы > ВНИМАНИЕ: -> - **ДЗ первого урока будет связано с [созданием небольшого CRUD приложения (в памяти, без DB) на JSP и сервлетах](http://danielniko.com/2012/04/17/simple-crud-using-jsp-servlet-and-mysql/)**. Введение будет, но предварительное знакомство не помешает. +> - **ДЗ первого урока будет связано с [созданием небольшого CRUD приложения (в памяти, без DB) на JSP и сервлетах](https://danielniko.wordpress.com/2012/04/17/simple-crud-using-jsp-servlet-and-mysql/)**. Введение будет, но предварительное знакомство не помешает. > - основы JavaSсript необходимы для понимания проекта, начиная с 8-го занятия! Все остальное - опционально. @@ -172,7 +190,7 @@ Java Enterprise Online Project #### Java (базовые вещи) - Интуит. Программирование на Java - 1й урок MasterJava: Многопоточность -- Основы Java garbage collection +- [Основы Java garbage collection](http://web.archive.org/web/20180831013112/https://ggenikus.github.io/blog/2014/05/04/gc) - Размер Java объектов - Введение в Java Reflection API - Структуры данных в картинках @@ -183,6 +201,7 @@ Java Enterprise Online Project - A Guide to Java 8 ### Туториалы, разное +- [Открытый курс: Spring Boot + HATEOAS](https://javaops.ru/view/bootjava) - [Что нужно знать о бэкенде новичку в веб-разработке](https://tproger.ru/translations/backend-web-development) - [Туториалы: Spring Framework, Hibernate, Java Core, JDBC](http://proselyte.net/tutorials/) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index f4e7412eb816..7c03206fe59c 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,8 +1,45 @@ # TopJava Release Notes + +### Topjava 21 +- **добавили документирование REST API: Swagger** +- мигрировали на JDK 15 и используем текстовые блоки +- Вынес `produces = MediaType.APPLICATION_JSON_VALUE` на уровень контроллеров +- Правильно используем [глабальные переменные в js](https://stackoverflow.com/a/5064235/548473) +- Зарефакторил `inputField.tag` +- Тестовые переменные переименовал из UPPERCASE в camelCase +- Из тестов сервисов убрал `throws Exception` (в IDEA больше не генерятся по умолчанию) +- **Мигрировали на Spring Boot 2.4.1** + +### Topjava 20 +- мигрировали на JDK 14 +- в `@SafeHtml` запрещаем весь html (`whitelistType = NONE`) +- в `topjava.common.js` в `makeEditable()` вместо объекта контекст передаю 3 параметра +- в UI контроллерах убрал префикс `ajax` +- из тестов сервисов убрал `repository`. При проверке через `assertThrows` он не требуется +- в `TestMatcher` сценарии сравнения сделал параметризируемыми (паттерн стратегия) +- в API добавили `/users/{id}/with-meals` (см. [двунаправленные отношения](https://www.codeflow.site/ru/article/jackson-bidirectional-relationships-and-infinite-recursion)) +- добавил `UserTestData.USER_WITH_MEALS_MATCHER` (проверки пользователя сразу с едой) и константу id `NOT_FOUND` + +### Topjava 19 +- Изменилась логика для интервалов времени (исключаем `endTime`) +- Заменил собственный `MessageUtil` велосипед на спринговый `MessageSourceAccessor` +- В ролях убрал префиксы `ROLE_` ([Role and GrantedAuthority](https://stackoverflow.com/a/19542316/548473)) +- Добавился удобный метод `int AbstractBaseEntity.id()` +- Фикс `Location` в `ProfileRestController.register` +- Фикс валидации `UniqueMailValidator` для REST update без `user.id` +- Заменил `jdbc.initLocation` на полный путь - IDEA не ругается +- В конфигурации `cargo-maven2-plugin` сделал [индивидуальный контекст приложения](https://stackoverflow.com/a/60797999/548473) +- Тесты + - Обновил даты еды на 2020г. + - Зарефакторил тесты сервисов на удаление - `NotFoundException` может бросаться при `delete()` + - В тестах контроллеров вернулся к реализации без обертки над `MockMvcRequestBuilders` + - Для `InMemory` тестов подключаю только `inmemory.xml` (добавил туда необходимую конфигурацию из `spring-app.xml`) + + ### Topjava 18 - В `ErrorType` добавил `HttpStatus status` -- В PostgreSQL обнаружилась бага: граничное значение `0:00` из за ошибок округления попадает в предыдущий интервал. +- В PostgreSQL обнаружилась бага: граничное значение `0:00` из-за ошибок округления попадает в предыдущий интервал. Мораль: всегда в тестах проверяйте граничные значения. Добавил этот случай в тестовые данные. - Изменил `MealRepository.getBetween` (принимаю `@Nullable LocalDate`). Изменились реализации. - Выделил метод `UserService.prepareAndSave` diff --git a/config/messages/app.properties b/config/messages/app.properties new file mode 100644 index 000000000000..ccd4c66b5397 --- /dev/null +++ b/config/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/config/messages/app_ru.properties b/config/messages/app_ru.properties new file mode 100644 index 000000000000..f22faf3cc513 --- /dev/null +++ b/config/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/cv.md b/cv.md index a24d881fdeba..73ca6a62be71 100644 --- a/cv.md +++ b/cv.md @@ -20,6 +20,7 @@ ### Наши истории (делимся опытом и успехом) ### Тесты/задачи онлайн: +- [Interviewing: the most profitable skill you can learn (pramp.com)](https://www.pramp.com/) - [Java Programming Test](https://tests4geeks.com/java) - game: test Java skills - Codility lesson tests @@ -28,7 +29,6 @@ - Sphere online judge - Codility programmers lessons - Hackerrank practice coding -- [Interviewing: the most profitable skill you can learn (pramp.com)](https://www.pramp.com/) - [start.interviewing.io](https://start.interviewing.io/) ## [Тестовое собеседование, самые спрашиваемые темы](http://javaops.ru/interview/test.html) @@ -88,8 +88,11 @@ - Яндекс агрегатор - HH - LinkedIn +- ХабрКарьера +- [headz.io](https://app.headz.io/candidates/new) - djinni.co (более актуально для Украины) + ## Как выжить на испытательном сроке - Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В процессе формулирования очень часто приходит ее решение. @@ -106,6 +109,11 @@ - Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи. - [**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) +- [ТОП-13 ошибок начинающего программиста](https://proglib.io/p/beginners-fails/) +- [25 ошибок начинающего программиста](https://habr.com/ru/post/413129/) +- [Путеводитель по синдрому самозванца](https://vc.ru/hr/167443-eshche-odin-putevoditel-po-sindromu-samozvanca-korni-prichiny-simptomy-i-posledstviya-chast-1) - [Нетехнические навыки](https://tproger.ru/experts/softskills-for-job) - +- Типичные ошибки начинающих программистов от JavaRush: + - [Часть 1](https://javarush.ru/groups/posts/3044-razbor-tipichnihkh-oshibok-nachinajujshikh-programmistov-chastjh-1) + - [Часть 2](https://javarush.ru/groups/posts/3055-razbor-tipichnihkh-oshibok-nachinajujshikh-programmistov-chastjh-2) ## [Отзывы по стажировке Topjava](https://vk.com/topic-74381644_30447246) diff --git a/description.md b/description.md index d2448ca99e57..0619c6d9516d 100644 --- a/description.md +++ b/description.md @@ -1,77 +1,72 @@ #### Разработка полнофункционального 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 для работы с ленивой загрузкой +- Основное внимание будет уделяться способам решения многочисленных проблем разработки в 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. +- Разбираются архитектурные паттерны: слои приложения и как правильно разбивать логику по слоям, когда нужно применять 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. +- Системы управления версиями +- 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 +- Поддержка 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 +- 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 -- Собеседование. Разработка ПО +- Encoding password +- CSRF (добавление в проект защиты от межсайтовой подделки запроса) +- form-login. Spring Security Taglib +- Handler interceptor +- Spring Exception Handling +- Смена локали +- Фильтрация JSON с помощью @JsonView +- Защита от XSS (Cross Site Scripting) +- Деплой в Heroku +- Локализация datatables, ошибок валидации +- Обработка ошибок 404 (NotFound) +- Доступ к AuthorizedUser +- Собеседование. Разработка ПО \ No newline at end of file diff --git a/graduation.md b/graduation.md index 6e4af3e023cc..442ce82b8c47 100644 --- a/graduation.md +++ b/graduation.md @@ -11,61 +11,77 @@ Build a voting system for deciding where to have lunch. * 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 before 11:00 we assume 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. +Each restaurant provides a 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. +P.P.S.: Assume 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, излишние делегирования и наследования - **не нужны!** +- **Сделай новый проект и добавляй туда из Topjava только то что нужно! Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - не нужны!** - **API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (frontend)** - **Сначала сделай основной сценарий по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом.** -*Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас конечно есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если конечно в ТЗ нет оговорок). Все свои вопросы и предложения и хотелки оформляйете отдельно (в `read.me` например). Если делаете что-то сложнее простейшего случая (например справочник еды)- обязательно напишите в read.me. Как и выбор стратегии кэширования.* +*Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас, конечно, есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если, конечно, в ТЗ нет оговорок). Все свои вопросы, предложения и хотелки оформляйте отдельно (в `read.me` например). Если делаете что-то сложнее простейшего случая (например, справочник еды) - обязательно напишите в read.me. Как и выбор стратегии кэширования.* > Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять _Антуан де Сент-Экзюпери_ - 1: **читаем ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять** -- 2: **тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу** +- 2: **тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу**. Самое худшее в коде - обращение в базу в цикле. - 3: **тщательно считайте количество запросов в вашем API для отображения нужной информации** -- 4: **учитывайте, что пользователей может быть ооочень много, а админов- мало** -- 5: в проекте (и тестовом задании на работу) в отличие от нашего учебного topjava оставляйте только необходимый для работы приложения код, ничего лишнего: - - 5.1 НЕ надо делать разные профили базы и работы с ней. - - 5.2 НЕ надо делать абстрактных контроллеров на всякий случай. - - 5.3 НЕ надо делать **классов репозиториев и сервисов**, если там нет ничего, кроме делегирования. - - 5.4 Из потребностей приложения (которую надо самим придумать) реализовывать только очевидные сценарии. Те.- НИЧЕГО ЛИШНЕГО. -- 6: базу лучше взять без установки (H2 или HSQLDB). Ваше приложение должно сразу запуститься, **без всяких настроек и переменных окружения** +- 4: учитывайте, что **пользователей может быть ОООЧЕНЬ много, а админов - МАЛО** +- 5: в проекте (и тестовом задании на работу), в отличие от нашего учебного topjava, оставляйте только необходимый для работы приложения код, ничего лишнего: + - 5.1 НЕ надо делать разные профили базы и работы с ней + - 5.2 НЕ надо делать абстрактных контроллеров на всякий случай + - 5.3 НЕ надо делать **классов репозиториев и сервисов**, если там нет ничего, кроме делегирования + - 5.4 Из потребностей приложения (которую надо самим придумать) реализовывать только очевидные сценарии. То есть НИЧЕГО ЛИШНЕГО. +- 6: База Данных + - берите без установки (H2 или HSQLDB). Одну!! Ваше приложение должно сразу запуститься, **без всяких настроек и переменных окружения** + - сделайте индексы к таблицам. Попробуйте обеспечит UNIQUE (один голос пользователя в день, один уникальный пункт меню в день). Следите за порядком полей в индексе + - **историю еды и голосований сделать НУЖНО! Есть базовые вещи, которые закладываются в архитектуру приложения и неочевидные доработки к ТЗ, которых лучше не делать.** + - при популировании добавте записи за сегодняшний день - now(), чтобы всегда были актуальные исходные данные + - таблицы обычно именуются в единственном числе. Исключение - users, т. к. user - зарезервированное слово. `date`/`timestamp` - зарезервированное слово, лучше избегать их при именовании - 7: по возможности сделать JUnit тесты - 8: уделяйте внимание обработке ошибок -- 9: далаем REST API в соответствии с концепцией REST - - [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) - - 10 Best Practices for Better RESTful API +- 9: делаем REST API в соответствии с концепцией REST (url в общем имеют вид`{ресурс}/{id_ресурсa}[/{подресурс}/{id_подресурсa}][параметры]`) + - **[15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/)** + - **10 Best Practices for Better RESTful API** - [REST resource hierarchy](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources) - 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 значениями (используйте `orElseThrow`). -- 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: Если используете кэширование, **тщательно продумайте, что надо кэшировать (самые частые запросы)**, а что нет (большие или редкозапрашиваемые данные)! +- 11: не размещайте логику приложения и преобразования в TO в слое доступа к DB +- 12: если приложению в объекте требуется только его id, используйте reference (как мы при сохранении еды вставляем туда юзера) +- 13: [Use for money in java app](http://stackoverflow.com/a/43051227/548473) +- 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 значениями (используйте `orElseThrow`) +- 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 их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverflow в коде обязательна! +- 21: проверьте, станет ли код проще с `@AuthenticationPrincipal` (урок 11, Доступ к AuthorizedUser). +- 22: обновление в базе делается через `update`, даже если `delete/insert` сократит java код на несколько строк +- 23: Кэширование + - необязательно, но желательно + - **тщательно продумайте, что надо кэшировать (самые частые запросы)**, а что нет (большие или редко запрашиваемые данные)! + - проверьте соответствие ключей к кэшу (параметры кэшируемого метода) с конфигурацией (например в singleNonExpiryCache, heap=1 в кэше может содержаться только ОДНО значение). +- 24: Валидация + - желательна + - одних аннотаций недостаточно. Должны быть `@Valid/@Validation` + - проверяйте входные данные при `create/update` **в контроллерах!** В TopJava это `ValidationUtil.checkNew()/assureIdConsistent()` +- 25: `readme.md`: + - Если задание на English, описание пишите также на English (тоже самое относится к языку резюме: вакансия на English предполагает ваше резюме на English) + - Требуемые примеры `curl` не прячьте, а пишите здесь! + - Проверяют люди с опытом в Java: не надо писать инструкций, как устанавливать Java и Maven:) ## Попробуйте подергать свое API по всем типичным сценариям ТЗ! - Удобно использовать? Можно сделать проще? Например чтобы проголосовать за ресторан залогиненному юзеру достаточно `restorauntId`. diff --git a/pom.xml b/pom.xml index 0b1c2896da5b..a8494d3a936d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ru.javawebinar topjava - jar + war 1.0-SNAPSHOT @@ -12,14 +12,36 @@ http://topjava.herokuapp.com/ - 1.8 + 15 UTF-8 UTF-8 + + 5.3.4 + 2.4.5 + 9.0.43 + + + 1.2.3 + 1.7.30 + + + 42.2.18 + + 4.13.2 + 3.19.0 + + + 5.4.28.Final + 6.2.0.Final + 3.0.1-b12 + + + 3.9.2 topjava - install + package org.apache.maven.plugins @@ -30,15 +52,230 @@ ${java.version} + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + -Dfile.encoding=UTF-8 + + + + + + + org.codehaus.cargo + cargo-maven3-plugin + 1.9.2 + + + tomcat9x + + UTF-8 + tomcat,datajpa + + + + org.postgresql + postgresql + + + + + + + src/main/resources/tomcat/context.xml + conf/Catalina/localhost/ + ${project.build.finalName}.xml + + + + + + ru.javawebinar + topjava + war + + ${project.build.finalName} + + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + org.springframework + spring-context-support + + + org.springframework.data + spring-data-jpa + ${spring-data-jpa.version} + + + + + org.hibernate + hibernate-core + ${hibernate.version} + + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + org.hibernate + hibernate-jcache + ${hibernate.version} + + + + + org.glassfish + javax.el + ${javax-el.version} + provided + + + + + javax.cache + cache-api + 1.1.0 + + + org.ehcache + ehcache + runtime + ${ehcache.version} + + + org.glassfish.jaxb + jaxb-runtime + + + + + + + org.glassfish.jaxb + jaxb-runtime + 2.4.0-b180830.0438 + + + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + + javax.servlet + jstl + 1.2 + + + + org.springframework + spring-webmvc + + + + + junit + junit + ${junit.version} + test + + + org.springframework + spring-test + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + hsqldb + + + org.hsqldb + hsqldb + 2.5.1 + + + + + + postgres + + + org.postgresql + postgresql + ${postgresql.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat.version} + provided + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + runtime + + + + true + + + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java deleted file mode 100644 index c2f9cc618f7c..000000000000 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.javawebinar.topjava; - -/** - * @see Demo application - * @see Initial project - */ -public class Main { - public static void main(String[] args) { - System.out.format("Hello TopJava Enterprise!"); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java new file mode 100644 index 000000000000..e4111a2afd28 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -0,0 +1,27 @@ +package ru.javawebinar.topjava; + +import org.springframework.util.ClassUtils; + +public class Profiles { + public static final String + JDBC = "jdbc", + JPA = "jpa", + DATAJPA = "datajpa"; + + public static final String REPOSITORY_IMPLEMENTATION = DATAJPA; + + public static final String + POSTGRES_DB = "postgres", + HSQL_DB = "hsqldb"; + + // Get DB profile depending of DB driver in classpath + public static String getActiveDbProfile() { + if (ClassUtils.isPresent("org.postgresql.Driver", null)) { + return POSTGRES_DB; + } else if (ClassUtils.isPresent("org.hsqldb.jdbcDriver", null)) { + return HSQL_DB; + } else { + throw new IllegalStateException("Could not find DB driver"); + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java new file mode 100644 index 000000000000..acb11612281c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -0,0 +1,70 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.Hibernate; +import org.springframework.data.domain.Persistable; +import org.springframework.util.Assert; + +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 implements Persistable { + public static final int START_SEQ = 100000; + + @Id + @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) + // @Column(name = "id", unique = true, nullable = false, columnDefinition = "integer default nextval('global_seq')") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") +// See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034 +// Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting + protected Integer id; + + protected AbstractBaseEntity() { + } + + protected AbstractBaseEntity(Integer id) { + this.id = id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + + public int id() { + Assert.notNull(id, "Entity must have id"); + return id; + } + + @Override + public boolean isNew() { + return this.id == null; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ":" + id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(Hibernate.getClass(o))) { + return false; + } + AbstractBaseEntity that = (AbstractBaseEntity) o; + return id != null && id.equals(that.id); + } + + @Override + public int hashCode() { + return id == null ? 0 : id; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java new file mode 100644 index 000000000000..e0734c307485 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.model; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; + + +@MappedSuperclass +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + @NotBlank + @Size(min = 2, max = 100) + @Column(name = "name", nullable = false) + protected String name; + + protected AbstractNamedEntity() { + } + + protected AbstractNamedEntity(Integer id, String name) { + super(id); + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return super.toString() + '(' + name + ')'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java new file mode 100644 index 000000000000..c00282b1e546 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -0,0 +1,114 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +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 >= :startDateTime AND m.dateTime < :endDateTime ORDER BY m.dateTime DESC + """), +// @NamedQuery(name = Meal.UPDATE, query = "UPDATE Meal m SET m.dateTime = :datetime, m.calories= :calories," + +// "m.description=:desc where m.id=:id and m.user.id=:userId") +}) +@Entity +@Table(name = "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) + @OnDelete(action = OnDeleteAction.CASCADE) + @NotNull + private User user; + + public Meal() { + } + + public Meal(LocalDateTime dateTime, String description, int calories) { + this(null, dateTime, description, calories); + } + + public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { + super(id); + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public LocalDate getDate() { + return dateTime.toLocalDate(); + } + + public LocalTime getTime() { + return dateTime.toLocalTime(); + } + + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setCalories(int calories) { + this.calories = calories; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public String toString() { + return "Meal{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java new file mode 100644 index 000000000000..acb7a276f6dc --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -0,0 +1,6 @@ +package ru.javawebinar.topjava.model; + +public enum Role { + USER, + 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..6bd6d74391cb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -0,0 +1,160 @@ +package ru.javawebinar.topjava.model; + +import javax.persistence.Entity; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OrderBy; +import javax.persistence.Table; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import org.hibernate.annotations.*; + +import org.hibernate.annotations.Cache; +import org.hibernate.validator.constraints.Range; +import org.springframework.util.CollectionUtils; + +import javax.persistence.*; +import java.util.*; + +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"), + @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u ORDER BY u.name, u.email"), +}) +@Entity +@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "users_unique_email_idx")}) +public class User extends AbstractNamedEntity { + + public static final String DELETE = "User.delete"; + public static final String BY_EMAIL = "User.getByEmail"; + public static final String ALL_SORTED = "User.getAllSorted"; + + @Column(name = "email", nullable = false, unique = true) + @Email + @NotBlank + @Size(max = 100) + 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(); + + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), + uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "user_roles_unique_idx")}) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) +// @Fetch(FetchMode.SUBSELECT) + @BatchSize(size = 200) + @JoinColumn(name = "user_id") //https://stackoverflow.com/a/62848296/548473 + @OnDelete(action= OnDeleteAction.CASCADE) + private Set roles; + + @Column(name = "calories_per_day", nullable = false, columnDefinition = "int default 2000") + @Range(min = 10, max = 10000) + private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) + @OrderBy("dateTime DESC") + private List meals; + + public User() { + } + + public User(User u) { + this(u.getId(), u.getName(), u.getEmail(), u.getPassword(), u.getCaloriesPerDay(), u.isEnabled(), u.getRegistered(), u.getRoles()); + } + + public User(Integer id, String name, String email, String password, Role role, 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, Date registered, Collection roles) { + super(id, name); + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + this.enabled = enabled; + this.registered = registered; + setRoles(roles); + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getRegistered() { + return registered; + } + + public void setRegistered(Date registered) { + this.registered = registered; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getCaloriesPerDay() { + return caloriesPerDay; + } + + public void setCaloriesPerDay(int caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public boolean isEnabled() { + return enabled; + } + + public Set getRoles() { + return roles; + } + + public String getPassword() { + return password; + } + + public void setRoles(Collection roles) { + this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles); + } + + public List getMeals() { + return meals; + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", email=" + email + + ", name=" + name + + ", enabled=" + enabled + + ", roles=" + roles + + ", caloriesPerDay=" + caloriesPerDay + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java new file mode 100644 index 000000000000..d740b9f7598e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java @@ -0,0 +1,22 @@ +package ru.javawebinar.topjava.repository; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +public class JpaUtil { + + @PersistenceContext + private EntityManager em; + + public void clear2ndLevelHibernateCache() { + Session s = (Session) em.getDelegate(); + SessionFactory sf = s.getSessionFactory(); +// sf.evict(User.class); +// sf.getCache().evictEntity(User.class, BaseEntity.START_SEQ); +// sf.getCache().evictEntityRegion(User.class); + sf.getCache().evictAllRegions(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java new file mode 100644 index 000000000000..1ad7f8d943d7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -0,0 +1,27 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; + +import java.time.LocalDateTime; +import java.util.List; + +public interface MealRepository { + // null if updated meal does not belong to userId + Meal save(Meal meal, int userId); + + // false if meal does not belong to userId + boolean delete(int id, int userId); + + // null if meal does not belong to userId + Meal get(int id, int userId); + + // ORDERED dateTime desc + List getAll(int userId); + + // ORDERED dateTime desc + List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId); + + default Meal getWithUser(int id, int userId) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java new file mode 100644 index 000000000000..9fecbddaaa19 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -0,0 +1,25 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public interface UserRepository { + // null if not found, when updated + User save(User user); + + // false if not found + boolean delete(int id); + + // null if not found + User get(int id); + + // null if not found + User getByEmail(String email); + + List getAll(); + + default User getWithMeals(int id) { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java new file mode 100644 index 000000000000..9aeef134f236 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudMealRepository.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; + +import java.time.LocalDateTime; +import java.util.List; + +@Transactional(readOnly = true) +public interface CrudMealRepository extends JpaRepository { + + @Modifying + @Transactional + @Query("DELETE FROM Meal m WHERE m.id=:id AND m.user.id=:userId") + int delete(@Param("id") int id, @Param("userId") int userId); + + @Query("SELECT m FROM Meal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC") + List getAll(@Param("userId") int userId); + + @Query("SELECT m from Meal m WHERE m.user.id=:userId AND m.dateTime >= :startDate AND m.dateTime < :endDate ORDER BY m.dateTime DESC") + List getBetweenHalfOpen(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("userId") int userId); + + @Query("SELECT m FROM Meal m JOIN FETCH m.user WHERE m.id = ?1 and m.user.id = ?2") + Meal getWithUser(int id, int userId); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java new file mode 100644 index 000000000000..806884a9c90b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -0,0 +1,23 @@ +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; +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("DELETE FROM User u WHERE u.id=:id") + int delete(@Param("id") int id); + + User getByEmail(String email); + + @EntityGraph(attributePaths = {"meals", "roles"}) + @Query("SELECT u FROM User u WHERE u.id=?1") + User getWithMeals(int id); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java new file mode 100644 index 000000000000..dc93b47e5c7b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaMealRepository.java @@ -0,0 +1,58 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public class DataJpaMealRepository implements MealRepository { + + private final CrudMealRepository crudMealRepository; + private final CrudUserRepository crudUserRepository; + + public DataJpaMealRepository(CrudMealRepository crudMealRepository, CrudUserRepository crudUserRepository) { + this.crudMealRepository = crudMealRepository; + this.crudUserRepository = crudUserRepository; + } + + @Override + @Transactional + public Meal save(Meal meal, int userId) { + if (!meal.isNew() && get(meal.getId(), userId) == null) { + return null; + } + meal.setUser(crudUserRepository.getOne(userId)); + return crudMealRepository.save(meal); + } + + @Override + public boolean delete(int id, int userId) { + return crudMealRepository.delete(id, userId) != 0; + } + + @Override + public Meal get(int id, int userId) { + return crudMealRepository.findById(id) + .filter(meal -> meal.getUser().getId() == userId) + .orElse(null); + } + + @Override + public List getAll(int userId) { + return crudMealRepository.getAll(userId); + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return crudMealRepository.getBetweenHalfOpen(startDateTime, endDateTime, userId); + } + + @Override + public Meal getWithUser(int id, int userId) { + return crudMealRepository.getWithUser(id, userId); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java new file mode 100644 index 000000000000..608c855e0a05 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepository.java @@ -0,0 +1,49 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +@Repository +public class DataJpaUserRepository implements UserRepository { + private static final Sort SORT_NAME_EMAIL = Sort.by(Sort.Direction.ASC, "name", "email"); + + private final CrudUserRepository crudRepository; + + public DataJpaUserRepository(CrudUserRepository crudRepository) { + this.crudRepository = crudRepository; + } + + @Override + public User save(User user) { + return crudRepository.save(user); + } + + @Override + public boolean delete(int id) { + return crudRepository.delete(id) != 0; + } + + @Override + public User get(int id) { + return crudRepository.findById(id).orElse(null); + } + + @Override + public User getByEmail(String email) { + return crudRepository.getByEmail(email); + } + + @Override + public List getAll() { + return crudRepository.findAll(SORT_NAME_EMAIL); + } + + @Override + public User getWithMeals(int id) { + return crudRepository.getWithMeals(id); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java new file mode 100644 index 000000000000..1f3fe5ffead0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -0,0 +1,84 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import 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 { + + private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class); + + private final JdbcTemplate jdbcTemplate; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final SimpleJdbcInsert insertMeal; + + public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("meals") + .usingGeneratedKeyColumns("id"); + + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + } + + @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("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 jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0; + } + + @Override + public Meal get(int id, int userId) { + List meals = jdbcTemplate.query( + "SELECT * FROM meals WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId); + return DataAccessUtils.singleResult(meals); + } + + @Override + public List getAll(int userId) { + return jdbcTemplate.query( + "SELECT * FROM meals WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId); + } + + @Override + public List getBetweenHalfOpen(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); + } +} 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..0d4baa50eef2 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -0,0 +1,75 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import 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) { + 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; + } + 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/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..300a920aea2f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -0,0 +1,64 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.MealRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@Transactional(readOnly = true) +public class JpaMealRepository implements MealRepository { + + @PersistenceContext + private EntityManager em; + + @Override + @Transactional + public Meal save(Meal meal, int userId) { + meal.setUser(em.getReference(User.class, userId)); + if (meal.isNew()) { + em.persist(meal); + return meal; + } else if (get(meal.id(), userId) == null) { + return null; + } + return em.merge(meal); + } + + @Override + @Transactional + public boolean delete(int id, int userId) { + return em.createNamedQuery(Meal.DELETE) + .setParameter("id", id) + .setParameter("userId", userId) + .executeUpdate() != 0; + } + + @Override + public Meal get(int id, int userId) { + Meal meal = em.find(Meal.class, id); + return meal != null && meal.getUser().getId() == userId ? meal : null; + } + + @Override + public List getAll(int userId) { + return em.createNamedQuery(Meal.ALL_SORTED, Meal.class) + .setParameter("userId", userId) + .getResultList(); + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return em.createNamedQuery(Meal.GET_BETWEEN, Meal.class) + .setParameter("userId", userId) + .setParameter("startDateTime", startDateTime) + .setParameter("endDateTime", endDateTime) + .getResultList(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java new file mode 100644 index 000000000000..9ade13354ac1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -0,0 +1,72 @@ +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 java.util.List; + +@Repository +@Transactional(readOnly = true) +public class JpaUserRepository implements UserRepository { + +/* + @Autowired + private SessionFactory sessionFactory; + + private Session openSession() { + return sessionFactory.getCurrentSession(); + } +*/ + + @PersistenceContext + private EntityManager em; + + @Override + @Transactional + public User save(User user) { + if (user.isNew()) { + em.persist(user); + return user; + } else { + return em.merge(user); + } + } + + @Override + public User get(int id) { + return em.find(User.class, id); + } + + @Override + @Transactional + public boolean delete(int id) { + +/* User ref = em.getReference(User.class, id); + em.remove(ref); + + Query query = em.createQuery("DELETE FROM User u WHERE u.id=:id"); + return query.setParameter("id", id).executeUpdate() != 0; +*/ + return em.createNamedQuery(User.DELETE) + .setParameter("id", id) + .executeUpdate() != 0; + } + + @Override + public User getByEmail(String email) { + List users = em.createNamedQuery(User.BY_EMAIL, User.class) + .setParameter(1, email) + .getResultList(); + return DataAccessUtils.singleResult(users); + } + + @Override + public List getAll() { + return em.createNamedQuery(User.ALL_SORTED, User.class).getResultList(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java new file mode 100644 index 000000000000..5e08c9e5a3dc --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; + +import java.time.LocalDate; +import java.util.List; + +import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfDayOrMin; +import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfNextDayOrMax; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service +public class MealService { + + 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 getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { + return repository.getBetweenHalfOpen(atStartOfDayOrMin(startDate), atStartOfNextDayOrMax(endDate), userId); + } + + public List getAll(int userId) { + return repository.getAll(userId); + } + + public void update(Meal meal, int userId) { + Assert.notNull(meal, "meal must not be null"); + checkNotFoundWithId(repository.save(meal, userId), meal.id()); + } + + 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 new file mode 100644 index 000000000000..2fa248762d62 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -0,0 +1,58 @@ +package ru.javawebinar.topjava.service; + +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; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service +public class UserService { + + private final UserRepository repository; + + public UserService(UserRepository repository) { + this.repository = repository; + } + + @CacheEvict(value = "users", allEntries = true) + public User create(User user) { + Assert.notNull(user, "user must not be null"); + return repository.save(user); + } + + @CacheEvict(value = "users", allEntries = true) + public void delete(int id) { + checkNotFoundWithId(repository.delete(id), id); + } + + public User get(int id) { + return checkNotFoundWithId(repository.get(id), id); + } + + public User getByEmail(String email) { + Assert.notNull(email, "email must not be null"); + return checkNotFound(repository.getByEmail(email), "email=" + email); + } + + @Cacheable("users") + public List getAll() { + return repository.getAll(); + } + + @CacheEvict(value = "users", allEntries = true) + public void update(User user) { + Assert.notNull(user, "user must not be null"); + checkNotFoundWithId(repository.save(user), user.id()); + } + + public User getWithMeals(int id) { + return checkNotFoundWithId(repository.getWithMeals(id), id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java new file mode 100644 index 000000000000..d14feae792b7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.to; + +import java.time.LocalDateTime; + +public class MealTo { + private final Integer id; + + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + private final 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; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public boolean isExcess() { + return excess; + } + + @Override + public String toString() { + return "MealTo{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + ", excess=" + excess + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java new file mode 100644 index 000000000000..a4665e2ae0e8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -0,0 +1,43 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +public class DateTimeUtil { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + // DB doesn't support LocalDate.MIN/MAX + private static final LocalDateTime MIN_DATE = LocalDateTime.of(1, 1, 1, 0, 0); + private static final LocalDateTime MAX_DATE = LocalDateTime.of(3000, 1, 1, 0, 0); + + private DateTimeUtil() { + } + + public static LocalDateTime atStartOfDayOrMin(LocalDate localDate) { + return localDate != null ? localDate.atStartOfDay() : MIN_DATE; + } + + public static LocalDateTime atStartOfNextDayOrMax(LocalDate localDate) { + return localDate != null ? localDate.plus(1, ChronoUnit.DAYS).atStartOfDay() : MAX_DATE; + } + + public static String toString(LocalDateTime ldt) { + return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); + } + + public static @Nullable + LocalDate parseLocalDate(@Nullable String str) { + return StringUtils.hasLength(str) ? LocalDate.parse(str) : null; + } + + public static @Nullable + LocalTime parseLocalTime(@Nullable String str) { + return StringUtils.hasLength(str) ? LocalTime.parse(str) : null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java new file mode 100644 index 000000000000..c99874f6ce3b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class MealsUtil { + public static final int DEFAULT_CALORIES_PER_DAY = 2000; + + private MealsUtil() { + } + + public static List getTos(Collection meals, int caloriesPerDay) { + return filterByPredicate(meals, caloriesPerDay, meal -> true); + } + + public static List getFilteredTos(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) { + return filterByPredicate(meals, caloriesPerDay, meal -> Util.isBetweenHalfOpen(meal.getTime(), startTime, endTime)); + } + + public static List filterByPredicate(Collection meals, int caloriesPerDay, Predicate filter) { + Map caloriesSumByDate = meals.stream() + .collect( + Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) +// Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum) + ); + + return meals.stream() + .filter(filter) + .map(meal -> createTo(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .collect(Collectors.toList()); + } + + private static MealTo createTo(Meal meal, boolean excess) { + return new MealTo(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java new file mode 100644 index 000000000000..0860f5c6bf3c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/Util.java @@ -0,0 +1,12 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.lang.Nullable; + +public class Util { + private Util() { + } + + public static > boolean isBetweenHalfOpen(T value, @Nullable T start, @Nullable T end) { + return (start == null || value.compareTo(start) >= 0) && (end == null || value.compareTo(end) < 0); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java new file mode 100644 index 000000000000..35d83de8e4bd --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.util; + + +import org.springframework.core.NestedExceptionUtils; +import org.springframework.lang.NonNull; +import ru.javawebinar.topjava.model.AbstractBaseEntity; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +public class ValidationUtil { + private ValidationUtil() { + } + + public static T checkNotFoundWithId(T object, int id) { + checkNotFoundWithId(object != null, id); + return object; + } + + 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.id() != id) { + throw new IllegalArgumentException(entity + " must be with id=" + id); + } + } + + // https://stackoverflow.com/a/65442410/548473 + @NonNull + public static Throwable getRootCause(@NonNull Throwable t) { + Throwable rootCause = NestedExceptionUtils.getRootCause(t); + return rootCause != null ? rootCause : t; + } +} \ 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/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java new file mode 100644 index 000000000000..fb667d351940 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -0,0 +1,85 @@ +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.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() { + 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.hasLength(request.getParameter("id"))) { + mealController.update(meal, getId(request)); + } else { + mealController.create(meal); + } + 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"); + } + case "create", "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); + } + 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); + } + default -> { + request.setAttribute("meals", mealController.getAll()); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + } + } + } + + 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 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/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java new file mode 100644 index 000000000000..4bad5863e3c6 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -0,0 +1,25 @@ +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 = AbstractBaseEntity.START_SEQ; + + private SecurityUtil() { + } + + public static int authUserId() { + return id; + } + + public static void setAuthUserId(int id) { + SecurityUtil.id = id; + } + + 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..bbfe35e3f0fa --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -0,0 +1,76 @@ +package ru.javawebinar.topjava.web.meal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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; + + 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.getBetweenInclusive(startDate, endDate, userId); + return MealsUtil.getFilteredTos(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(), startTime, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java new file mode 100644 index 000000000000..0000f1c1e02f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -0,0 +1,51 @@ +package ru.javawebinar.topjava.web.user; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +public abstract class AbstractUserController { + protected final Logger log = LoggerFactory.getLogger(getClass()); + + @Autowired + private UserService service; + + 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..b37a8ed6c8a5 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -0,0 +1,40 @@ +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 + 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..7d3702c31c46 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -0,0 +1,22 @@ +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() { + 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 diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml new file mode 100644 index 000000000000..05589f71f06e --- /dev/null +++ b/src/main/resources/cache/ehcache.xml @@ -0,0 +1,25 @@ + + + + + + + + + + 5 + + 5000 + + + + + + + 1 + + + + diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties new file mode 100644 index 000000000000..17c03ef4ebda --- /dev/null +++ b/src/main/resources/db/hsqldb.properties @@ -0,0 +1,11 @@ +#database.url=jdbc:hsqldb:file:D:/temp/topjava + +database.url=jdbc:hsqldb:mem:topjava +database.username=sa +database.password= + +database.init=true +jdbc.initLocation=classpath:db/initDB_hsql.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql new file mode 100644 index 000000000000..af0ff307cb0a --- /dev/null +++ b/src/main/resources/db/initDB.sql @@ -0,0 +1,37 @@ +DROP TABLE IF EXISTS user_roles; +DROP TABLE IF EXISTS meals; +DROP TABLE IF EXISTS users; +DROP SEQUENCE IF EXISTS global_seq; + +CREATE SEQUENCE global_seq START WITH 100000; + +CREATE TABLE users +( + id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), + name VARCHAR NOT NULL, + email VARCHAR NOT NULL, + password VARCHAR NOT NULL, + registered TIMESTAMP DEFAULT now() NOT NULL, + enabled BOOL DEFAULT TRUE NOT NULL, + calories_per_day INTEGER DEFAULT 2000 NOT NULL +); +CREATE UNIQUE INDEX users_unique_email_idx ON users (email); + +CREATE TABLE user_roles +( + user_id INTEGER NOT NULL, + role VARCHAR, + CONSTRAINT user_roles_idx UNIQUE (user_id, role), + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE meals +( + id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), + user_id INTEGER NOT NULL, + date_time TIMESTAMP NOT NULL, + description TEXT NOT NULL, + calories INT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); +CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals (user_id, date_time); \ No newline at end of file diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql new file mode 100644 index 000000000000..37f2da1bf3d8 --- /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/populateDB.sql b/src/main/resources/db/populateDB.sql new file mode 100644 index 000000000000..a6c79fb907dc --- /dev/null +++ b/src/main/resources/db/populateDB.sql @@ -0,0 +1,23 @@ +DELETE FROM user_roles; +DELETE FROM meals; +DELETE FROM users; +ALTER SEQUENCE global_seq RESTART WITH 100000; + +INSERT INTO users (name, email, password) +VALUES ('User', 'user@yandex.ru', 'password'), + ('Admin', 'admin@gmail.com', 'admin'); + +INSERT INTO user_roles (role, user_id) +VALUES ('USER', 100000), + ('ADMIN', 100001); + +INSERT INTO meals (date_time, description, calories, user_id) +VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000), + ('2020-01-30 13:00:00', 'Обед', 1000, 100000), + ('2020-01-30 20:00:00', 'Ужин', 500, 100000), + ('2020-01-31 0:00:00', 'Еда на граничное значение', 100, 100000), + ('2020-01-31 10:00:00', 'Завтрак', 500, 100000), + ('2020-01-31 13:00:00', 'Обед', 1000, 100000), + ('2020-01-31 20:00:00', 'Ужин', 510, 100000), + ('2020-01-31 14:00:00', 'Админ ланч', 510, 100001), + ('2020-01-31 21:00:00', 'Админ ужин', 1500, 100001); \ No newline at end of file diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties new file mode 100644 index 000000000000..63817b2c85bf --- /dev/null +++ b/src/main/resources/db/postgres.properties @@ -0,0 +1,13 @@ +#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 + +database.init=true +jdbc.initLocation=classpath:db/initDB.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties new file mode 100644 index 000000000000..2e073681ad16 --- /dev/null +++ b/src/main/resources/db/tomcat.properties @@ -0,0 +1,5 @@ +database.init=false +jdbc.initLocation=initDB.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 000000000000..809d4c9c3d50 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,30 @@ + + + + + + + + ${TOPJAVA_ROOT}/log/topjava.log + + + UTF-8 + %date %-5level %logger{50}.%M:%L - %msg%n + + + + + + UTF-8 + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n + + + + + + + + + + + diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml new file mode 100644 index 000000000000..d57b656aeb75 --- /dev/null +++ b/src/main/resources/spring/spring-app.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-cache.xml b/src/main/resources/spring/spring-cache.xml new file mode 100644 index 000000000000..73325fee065f --- /dev/null +++ b/src/main/resources/spring/spring-cache.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml new file mode 100644 index 000000000000..2b62f074828b --- /dev/null +++ b/src/main/resources/spring/spring-db.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..ce12ddcc64d5 --- /dev/null +++ b/src/main/resources/spring/spring-mvc.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/tomcat/context.xml b/src/main/resources/tomcat/context.xml new file mode 100644 index 000000000000..9311d5904aea --- /dev/null +++ b/src/main/resources/tomcat/context.xml @@ -0,0 +1,57 @@ + + + + + + + + WEB-INF/web.xml + ${catalina.base}/conf/web.xml + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp new file mode 100644 index 000000000000..5b5efe57e65b --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -0,0 +1,6 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ 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 new file mode 100644 index 000000000000..0935c441a36b --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp @@ -0,0 +1,4 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ 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/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp new file mode 100644 index 000000000000..6d77694e3406 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -0,0 +1,9 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> + + + + <spring:message code="app.title"/> + + \ 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 new file mode 100644 index 000000000000..84719196538d --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/index.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + + + +
+
+
+ : + + +
+ + + \ 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 new file mode 100644 index 000000000000..98a6f48735dc --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/mealForm.jsp @@ -0,0 +1,34 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ 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/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp new file mode 100644 index 000000000000..702d9ef32efd --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -0,0 +1,66 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ 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" %> + + + Meals + + + +
+

Home

+
+

Meals

+
+ +
+
From Date (inclusive):
+
+
+
+
To Date (inclusive):
+
+
+
+
From Time (inclusive):
+
+
+
+
To Time (exclusive):
+
+
+ +
+
+ Add Meal +

+ + + + + + + + + + + + + + + + + + + + +
DateDescriptionCalories
+ <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> + <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> + <%--${fn:replace(meal.dateTime, 'T', ' ')}--%> + ${fn:formatDateTime(meal.dateTime)} + ${meal.description}${meal.calories}UpdateDelete
+
+ + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp new file mode 100644 index 000000000000..dad5f1b1bce5 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -0,0 +1,39 @@ +<%@ page contentType="text/html;charset=UTF-8" %> +<%@ 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" %> + + + + + + +
+

+ + + + + + + + + + + + + + + + + + + + + +
${user.email}${user.roles}<%=user.isEnabled()%> +
+
+ + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld new file mode 100644 index 000000000000..d138fecdbfb5 --- /dev/null +++ b/src/main/webapp/WEB-INF/tld/functions.tld @@ -0,0 +1,16 @@ + + + + 1.0 + functions + http://topjava.javawebinar.ru/functions + + + formatDateTime + ru.javawebinar.topjava.util.DateTimeUtil + java.lang.String toString(java.time.LocalDateTime) + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..b7558a705aa6 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,39 @@ + + + Topjava + + + spring.profiles.default + postgres,datajpa + + + + contextConfigLocation + + classpath:spring/spring-app.xml + classpath:spring/spring-db.xml + + + + + + org.springframework.web.context.ContextLoaderListener + + + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:spring/spring-mvc.xml + + 1 + + + mvc-dispatcher + / + + diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css new file mode 100644 index 000000000000..7140abce2e3f --- /dev/null +++ b/src/main/webapp/resources/css/style.css @@ -0,0 +1,40 @@ +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; +} + +header, footer { + background: none repeat scroll 0 0 #A6C9E2; + color: #2E6E9E; + font-size: 20px; + padding: 5px 20px; + margin: 6px 0; +} + +header, footer { + background: none repeat scroll 0 0 #A6C9E2; + color: #2E6E9E; + font-size: 20px; + padding: 5px 20px; + margin: 6px 0; +} 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..2d819f84e5eb --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java @@ -0,0 +1,13 @@ +package ru.javawebinar.topjava; + +import org.springframework.lang.NonNull; +import org.springframework.test.context.ActiveProfilesResolver; + +//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver +public class ActiveDbProfileResolver implements ActiveProfilesResolver { + @Override + public @NonNull + String[] resolve(@NonNull Class aClass) { + return new String[]{Profiles.getActiveDbProfile()}; + } +} 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..e19989f96dd4 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Meal; + +import java.time.Month; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import static java.time.LocalDateTime.of; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class MealTestData { + public static final TestMatcher MEAL_MATCHER = TestMatcher.usingIgnoringFieldsComparator("user"); + + public static final int NOT_FOUND = 10; + public static final int MEAL1_ID = START_SEQ + 2; + public static final int ADMIN_MEAL_ID = START_SEQ + 9; + + public static final Meal meal1 = new Meal(MEAL1_ID, of(2020, Month.JANUARY, 30, 10, 0), "Завтрак", 500); + public static final Meal meal2 = new Meal(MEAL1_ID + 1, of(2020, Month.JANUARY, 30, 13, 0), "Обед", 1000); + public static final Meal meal3 = new Meal(MEAL1_ID + 2, of(2020, Month.JANUARY, 30, 20, 0), "Ужин", 500); + public static final Meal meal4 = new Meal(MEAL1_ID + 3, of(2020, Month.JANUARY, 31, 0, 0), "Еда на граничное значение", 100); + public static final Meal meal5 = new Meal(MEAL1_ID + 4, of(2020, Month.JANUARY, 31, 10, 0), "Завтрак", 500); + public static final Meal meal6 = new Meal(MEAL1_ID + 5, of(2020, Month.JANUARY, 31, 13, 0), "Обед", 1000); + public static final Meal meal7 = new Meal(MEAL1_ID + 6, of(2020, Month.JANUARY, 31, 20, 0), "Ужин", 510); + public static final Meal adminMeal1 = new Meal(ADMIN_MEAL_ID, of(2020, Month.JANUARY, 31, 14, 0), "Админ ланч", 510); + public static final Meal adminMeal2 = new Meal(ADMIN_MEAL_ID + 1, of(2020, Month.JANUARY, 31, 21, 0), "Админ ужин", 1500); + + public static final List meals = List.of(meal7, meal6, meal5, meal4, meal3, meal2, meal1); + + public static Meal getNew() { + return new Meal(null, of(2020, Month.FEBRUARY, 1, 18, 0), "Созданный ужин", 300); + } + + public static Meal getUpdated() { + return new Meal(MEAL1_ID, meal1.getDateTime().plus(2, ChronoUnit.MINUTES), "Обновленный завтрак", 200); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java new file mode 100644 index 000000000000..d809f025d043 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava; + +import org.springframework.context.support.GenericXmlApplicationContext; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.web.meal.MealRestController; +import ru.javawebinar.topjava.web.user.AdminRestController; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +public class SpringMain { + public static void main(String[] args) { + // java 7 automatic resource management (ARM) + 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.ADMIN)); + System.out.println(); + + MealRestController mealController = appCtx.getBean(MealRestController.class); + List filteredMealsWithExcess = + mealController.getBetween( + LocalDate.of(2020, Month.JANUARY, 30), LocalTime.of(7, 0), + LocalDate.of(2020, Month.JANUARY, 31), LocalTime.of(11, 0)); + filteredMealsWithExcess.forEach(System.out::println); + System.out.println(); + System.out.println(mealController.getBetween(null, null, null, null)); + } + } +} diff --git a/src/test/java/ru/javawebinar/topjava/TestMatcher.java b/src/test/java/ru/javawebinar/topjava/TestMatcher.java new file mode 100644 index 000000000000..131cdef8def3 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TestMatcher.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestMatcher { + private final String[] fieldsToIgnore; + + private TestMatcher(String... fieldsToIgnore) { + this.fieldsToIgnore = fieldsToIgnore; + } + + public static TestMatcher usingIgnoringFieldsComparator(String... fieldsToIgnore) { + return new TestMatcher<>(fieldsToIgnore); + } + + public void assertMatch(T actual, T expected) { + assertThat(actual).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(expected); + } + + public void assertMatch(Iterable actual, T... expected) { + assertMatch(actual, List.of(expected)); + } + + public void assertMatch(Iterable actual, Iterable expected) { + assertThat(actual).usingElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(expected); + } +} 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..fdd3d8779dbc --- /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 final StringBuilder results = new StringBuilder(); + + // 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"); + } + }; + + // https://dzone.com/articles/applying-new-jdk-11-string-methods + private static final String DELIM = "-".repeat(103); + + 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/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java new file mode 100644 index 000000000000..979401e03432 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -0,0 +1,35 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; + +import java.util.Collections; +import java.util.Date; + +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class UserTestData { + public static final TestMatcher USER_MATCHER = TestMatcher.usingIgnoringFieldsComparator("registered", "roles", "meals"); + + public static final int USER_ID = START_SEQ; + public static final int ADMIN_ID = START_SEQ + 1; + public static final int NOT_FOUND = 10; + + public static final User user = new User(USER_ID, "User", "user@yandex.ru", "password", Role.USER); + public static final User admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ADMIN); + + public static User getNew() { + return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.USER)); + } + + public static User getUpdated() { + User updated = new User(user); + updated.setEmail("update@gmail.com"); + updated.setName("UpdatedName"); + updated.setCaloriesPerDay(330); + updated.setPassword("newPass"); + updated.setEnabled(false); + updated.setRoles(Collections.singletonList(Role.ADMIN)); + return updated; + } +} diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java new file mode 100644 index 000000000000..a7cd8a47fba5 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import ru.javawebinar.topjava.model.AbstractBaseEntity; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class InMemoryBaseRepository { + + static final AtomicInteger counter = new AtomicInteger(START_SEQ); + + final Map map = new ConcurrentHashMap<>(); + + public T save(T entity) { + Objects.requireNonNull(entity, "Entity must not be null"); + if (entity.isNew()) { + entity.setId(counter.getAndIncrement()); + map.put(entity.getId(), entity); + return entity; + } + return map.computeIfPresent(entity.getId(), (id, oldT) -> entity); + } + + public boolean delete(int id) { + return map.remove(id) != null; + } + + public T get(int id) { + return map.get(id); + } + + Collection getCollection() { + return map.values(); + } + + void put(T entity) { + Objects.requireNonNull(entity, "Entity must not be null"); + map.put(entity.id(), entity); + } +} \ 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 new file mode 100644 index 000000000000..51d2afaa5ca2 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -0,0 +1,81 @@ +package ru.javawebinar.topjava.repository.inmemory; + +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; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +@Repository +public class InMemoryMealRepository implements MealRepository { + private static final Logger log = LoggerFactory.getLogger(InMemoryMealRepository.class); + + // Map userId -> mealRepository + private final Map> usersMealsMap = new ConcurrentHashMap<>(); + + { + var userMeals = new InMemoryBaseRepository(); + MealTestData.meals.forEach(userMeals::put); + usersMealsMap.put(UserTestData.USER_ID, userMeals); + } + + + @Override + public Meal save(Meal meal, int userId) { + Objects.requireNonNull(meal, "meal must not be null"); + var meals = usersMealsMap.computeIfAbsent(userId, uid -> new InMemoryBaseRepository<>()); + return meals.save(meal); + } + + @PostConstruct + public void postConstruct() { + log.info("+++ PostConstruct"); + } + + @PreDestroy + public void preDestroy() { + log.info("+++ PreDestroy"); + } + + @Override + public boolean delete(int id, int userId) { + var meals = usersMealsMap.get(userId); + return meals != null && meals.delete(id); + } + + @Override + public Meal get(int id, int userId) { + var meals = usersMealsMap.get(userId); + return meals == null ? null : meals.get(id); + } + + @Override + public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return filterByPredicate(userId, meal -> Util.isBetweenHalfOpen(meal.getDateTime(), startDateTime, endDateTime)); + } + + @Override + public List getAll(int userId) { + return filterByPredicate(userId, meal -> true); + } + + private List filterByPredicate(int userId, Predicate filter) { + var meals = usersMealsMap.get(userId); + return meals == null ? Collections.emptyList() : + meals.getCollection().stream() + .filter(filter) + .sorted(Comparator.comparing(Meal::getDateTime).reversed()) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java new file mode 100644 index 000000000000..3f4159313c46 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static ru.javawebinar.topjava.UserTestData.admin; +import static ru.javawebinar.topjava.UserTestData.user; + + +@Repository +public class InMemoryUserRepository extends InMemoryBaseRepository implements UserRepository { + + public void init() { + map.clear(); + put(user); + put(admin); + counter.getAndSet(UserTestData.ADMIN_ID + 1); + } + + @Override + public List getAll() { + return getCollection().stream() + .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) + .collect(Collectors.toList()); + } + + @Override + public User getByEmail(String email) { + Objects.requireNonNull(email, "email must not be null"); + return getCollection().stream() + .filter(u -> email.equals(u.getEmail())) + .findFirst() + .orElse(null); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java new file mode 100644 index 000000000000..84e59dabfd4a --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -0,0 +1,113 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +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 org.junit.Assert.assertThrows; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +public abstract class AbstractMealServiceTest extends AbstractServiceTest { + + @Autowired + protected MealService service; + + @Test + public void delete() { + service.delete(MEAL1_ID, USER_ID); + assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, USER_ID)); + } + + @Test + public void deleteNotFound() { + assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND, USER_ID)); + } + + @Test + public void deleteNotOwn() { + assertThrows(NotFoundException.class, () -> service.delete(MEAL1_ID, ADMIN_ID)); + } + + @Test + public void create() { + Meal created = service.create(getNew(), USER_ID); + int newId = created.id(); + Meal newMeal = getNew(); + newMeal.setId(newId); + MEAL_MATCHER.assertMatch(created, newMeal); + MEAL_MATCHER.assertMatch(service.get(newId, USER_ID), newMeal); + } + + @Test + public void duplicateDateTimeCreate() { + assertThrows(DataAccessException.class, () -> + service.create(new Meal(null, meal1.getDateTime(), "duplicate", 100), USER_ID)); + } + + + @Test + public void get() { + Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); + MEAL_MATCHER.assertMatch(actual, adminMeal1); + } + + @Test + public void getNotFound() { + assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND, USER_ID)); + } + + @Test + public void getNotOwn() { + assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, ADMIN_ID)); + } + + @Test + public void update() { + Meal updated = getUpdated(); + service.update(updated, USER_ID); + MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated()); + } + + @Test + public void updateNotOwn() { + NotFoundException exception = assertThrows(NotFoundException.class, () -> service.update(getUpdated(), ADMIN_ID)); + Assert.assertEquals("Not found entity with id=" + MEAL1_ID, exception.getMessage()); + MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), meal1); + } + + @Test + public void getAll() { + MEAL_MATCHER.assertMatch(service.getAll(USER_ID), meals); + } + + @Test + public void getBetweenInclusive() { + MEAL_MATCHER.assertMatch(service.getBetweenInclusive( + LocalDate.of(2020, Month.JANUARY, 30), + LocalDate.of(2020, Month.JANUARY, 30), USER_ID), + meal3, meal2, meal1); + } + + @Test + public void getBetweenWithNullDates() { + MEAL_MATCHER.assertMatch(service.getBetweenInclusive(null, null, USER_ID), meals); + } + + @Test + public void createWithException() throws Exception { + validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID)); + validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, null, "Description", 300), USER_ID)); + validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID)); + validateRootCause(ConstraintViolationException.class, () -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 5001), USER_ID)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java new file mode 100644 index 000000000000..c53a5b715df0 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava.service; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.rules.ExternalResource; +import org.junit.rules.Stopwatch; +import org.junit.runner.RunWith; +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.TimingRules; + +import static org.junit.Assert.assertThrows; +import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause; + +@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 { + + @ClassRule + public static ExternalResource summary = TimingRules.SUMMARY; + + @Rule + public Stopwatch stopwatch = TimingRules.STOPWATCH; + + // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 + public void validateRootCause(Class rootExceptionClass, Runnable runnable) { + assertThrows(rootExceptionClass, () -> { + try { + runnable.run(); + } catch (Exception e) { + throw getRootCause(e); + } + }); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java new file mode 100644 index 000000000000..92800e249445 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -0,0 +1,104 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.dao.DataAccessException; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.Role; +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 ru.javawebinar.topjava.repository.JpaUtil; + +import static org.junit.Assert.assertThrows; +import static ru.javawebinar.topjava.UserTestData.*; + +public abstract class AbstractUserServiceTest extends AbstractServiceTest { + + @Autowired + protected UserService service; + + @Autowired + private CacheManager cacheManager; + + @Autowired + protected JpaUtil jpaUtil; + + @Before + public void setup() { + cacheManager.getCache("users").clear(); + jpaUtil.clear2ndLevelHibernateCache(); + } + + @Test + public void create() { + User created = service.create(getNew()); + int newId = created.id(); + User newUser = getNew(); + newUser.setId(newId); + USER_MATCHER.assertMatch(created, newUser); + USER_MATCHER.assertMatch(service.get(newId), newUser); + } + + @Test + public void duplicateMailCreate() { + assertThrows(DataAccessException.class, () -> + service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.USER))); + } + + @Test + public void delete() { + service.delete(USER_ID); + assertThrows(NotFoundException.class, () -> service.get(USER_ID)); + } + + @Test + public void deletedNotFound() { + assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND)); + } + + @Test + public void get() { + User user = service.get(USER_ID); + USER_MATCHER.assertMatch(user, UserTestData.user); + } + + @Test + public void getNotFound() { + assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND)); + } + + @Test + public void getByEmail() { + User user = service.getByEmail("admin@gmail.com"); + USER_MATCHER.assertMatch(user, admin); + } + + @Test + public void update() { + User updated = getUpdated(); + service.update(updated); + USER_MATCHER.assertMatch(service.get(USER_ID), getUpdated()); + } + + @Test + public void getAll() { + List all = service.getAll(); + USER_MATCHER.assertMatch(all, admin, user); + } + + @Test + public void createWithException() throws Exception { + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.USER))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", Role.USER))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.USER))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Set.of()))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Set.of()))); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java new file mode 100644 index 000000000000..61847184a761 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -0,0 +1,30 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.Assert; +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; +import static ru.javawebinar.topjava.UserTestData.admin; + +@ActiveProfiles(DATAJPA) +public class DataJpaMealServiceTest extends AbstractMealServiceTest { + @Test + public void getWithUser() { + Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID); + MEAL_MATCHER.assertMatch(adminMeal, adminMeal1); + UserTestData.USER_MATCHER.assertMatch(adminMeal.getUser(), admin); + } + + @Test + public void getWithUserNotFound() { + Assert.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 new file mode 100644 index 000000000000..32bcf5ef40ed --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -0,0 +1,30 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.UserTestData; +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.USER_ID; +import static ru.javawebinar.topjava.UserTestData.USER_MATCHER; + +@ActiveProfiles(DATAJPA) +public class DataJpaUserServiceTest extends AbstractUserServiceTest { + @Test + public void getWithMeals() { + User user = service.getWithMeals(USER_ID); + USER_MATCHER.assertMatch(user, UserTestData.user); + MealTestData.MEAL_MATCHER.assertMatch(user.getMeals(), MealTestData.meals); + } + + @Test + public void getWithMealsNotFound() { + Assert.assertThrows(NotFoundException.class, + () -> service.getWithMeals(1)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java new file mode 100644 index 000000000000..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 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..9102d2104634 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -0,0 +1,41 @@ +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.repository.inmemory.InMemoryUserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +@ContextConfiguration({"classpath:spring/spring-app.xml", "classpath:spring/inmemory.xml"}) +@RunWith(SpringRunner.class) +public class InMemoryAdminRestControllerSpringTest { + + @Autowired + private AdminRestController controller; + + @Autowired + private InMemoryUserRepository repository; + + @Before + public void setup() { + repository.init(); + } + + @Test + public void delete() { + controller.delete(USER_ID); + Assert.assertNull(repository.get(USER_ID)); + } + + @Test + public void deleteNotFound() { + Assert.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND)); + } +} \ 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 new file mode 100644 index 000000000000..14c60d960a09 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -0,0 +1,52 @@ +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.repository.inmemory.InMemoryUserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Arrays; + +import static ru.javawebinar.topjava.UserTestData.NOT_FOUND; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +public class InMemoryAdminRestControllerTest { + private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); + + private static ConfigurableApplicationContext appCtx; + private static AdminRestController controller; + private static InMemoryUserRepository repository; + + @BeforeClass + public 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); + repository = appCtx.getBean(InMemoryUserRepository.class); + } + + @AfterClass + public static void afterClass() { + appCtx.close(); + } + + @Before + public void setup() { + // re-initialize + repository.init(); + } + + @Test + public void delete() { + controller.delete(USER_ID); + Assert.assertNull(repository.get(USER_ID)); + } + + @Test + public void deleteNotFound() { + Assert.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..803655475302 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,32 @@ + + + + true + + + + + UTF-8 + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n + + + + + + UTF-8 + %magenta(%msg%n) + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml new file mode 100644 index 000000000000..c6a2710cbd88 --- /dev/null +++ b/src/test/resources/spring/inmemory.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file