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..f5f58000338e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +# https://docs.travis-ci.com/user/languages/java/ +language: java +jdk: openjdk14 + +#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 37200eff9508..beb9b576d87a 100644 --- a/README.md +++ b/README.md @@ -1,254 +1,143 @@ -[Java Enterprise Online Project](https://javaops.ru/view/topjava) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/d1a6f6d0b4404f32bd59394074a3c1b6)](https://www.codacy.com/manual/javawebinar/topjava) +[![Build Status](https://travis-ci.org/JavaWebinar/topjava.svg?branch=master)](https://travis-ci.org/JavaWebinar/topjava) + +Java Enterprise Online Project =============================== -Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), DataTables, jQuery + plugins, Java 8 Stream and Time API и хранением в базах данных Postgresql и HSQLDB. -![topjava_structure](https://javaops.ru/static/images/projects/top-scheme.jpg) - - Когда вы слышите что-то, вы забываете это. - Когда вы видите что-то, вы запоминаете это. - Но только когда вы начинаете делать это, - вы начинаете понимать это - - Старинная китайская поговорка - -## Описание и план проекта -### Демо разрабатываемого приложения -### [Изменения проекта (Release Notes)](ReleaseNotes.md) -### Требования к участникам, Wiki -### Составление резюме, подготовка к интервью, поиск работы - -Обновленное вводное занятие (обязательно смотреть все видео) -=============== -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. [Вступление, история, команда, источники](doc/video1.md) - -## Обзор наиболее востребованных технологий, которые будут изучаться на курсе TopJava -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2.1. [Часть 1: инфраструктура](doc/video2.1.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2.2. [Часть 2: frameworks Spring, ORM](doc/video2.2.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2.3. [Часть 3: тренды](doc/video2.3.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2.3. [Часть 4: обзор разрабатываемого приложения](doc/video2.4.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. [Рекомендуемые подходы к обучению на курсе](doc/video3.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. [Структура приложения (многоуровневая архитектура)](doc/video4.md) -### [Демо приложения](http://javaops-demo.ru/topjava) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. [Системы управления версиями. Git](doc/video5-vcs-git.md) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Работа с проектом (выполнять инструкции) -- **ВНИМАНИЕ: выбирайте для проекта простой путь без пробелов и русских букв, например, `c:\projects\topjava\` (Windows). Иначе впоследствии будут проблемы** -- **Плагин Git Intergation уже не требуется, а вкладку `Version control` в IDEA переименовали в `Git`** -- **C Ultimate IDEA сейчас сложности**, надеемся что временные. - - Скачать IDEA и установливать плагины [можно по инструкции](https://github.com/JavaOPs/topjava/wiki/IDEA#download) - - После Trial 30 дней [способы продлить использование](https://github.com/JavaOPs/topjava/wiki/IDEA#licence) - -Для переключения режима отображения изменений из вкладки `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) (скачать и положить в каталог вашего проекта) - -> Проект постоянно улучшается, поэтому видео иногда отличается от кода проекта. Изменения указываю после видео: -> - переименовал класс `UserMealWithExceed` и его поле `exceed` в `UserMealWithExcess.excess` -> - в `UserMeals/UserMealWithExcess` поля изменились на `private` -> - обновил данные `UserMealsUtil.meals` и переименовал некоторые переменные, поля и методы -> - добавил `UserMealWithExcess.toString()` и метод для выполнения _Optional домашнего задания_ -> - метод фильтрации в `TimeUtil` переименовал в `isBetweenHalfOpen` (также изменилась логика сравнения: `startTime` включается в интервал, а `endTime` - не включается) - -### GitHub поменял политику: теперь пушить нужно через токен. IDEA предложит его сгенерировать при пуше, или можно [создать токен в настройках](https://www.jetbrains.com/help/idea/github.html#register-account) -- [Способы авторизации в GitHub](https://topjava.ru/blog/vvedeniye-v-git-github-ustanovka-i-nastroyka#6) - -## Инструкция по шагам (из видео): -- Установить ПО (Git, JDK8, IntelliJ IDEA, Maven) -- Создать аккаунт на GitHub -- Сделать Fork **ЭТОГО** проекта (https://github.com/JavaOPs/topjava) -- Сделать локальный репозиторий проекта: -
git clone https://github.com/[Ваш аккаунт]/topjava.git
- -> Вместо Fork можно сделать [клонирование проекта](https://github.com/JavaOPs/topjava//Git#user-content-Клонирование-проекта): он не будет привязан к исходному https://github.com/JavaOPs/topjava и у него не будет истории. - -- Открыть и настроить проект в IDEA - - Выставить кодировку UTF-8 в консоли - - Поставить кодировку UTF-8 - - Опционально: поменять шрифт по умолчанию на DejaVu или на **новый [JetBrains Mono](https://habr.com/ru/company/jugru/news/t/484134/)** -- По ходу видео сделать `Apply Patch...` скачанного патча `Prepare_to_HW0.patch` -- Закоммитить и запушить изменения (`commit` + `push`) -- Сделать ветку домашнего задания -- Выполнить задание и залить на GitHub (`commit` + `push`) -- Переключиться в основную ветку проекта `master`. - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. [Maven](https://drive.google.com/file/d/1qEJTwv9FNUQjx-y9MSydH01xaAne0-hu) -- [Как установить Maven 3 на Ubuntu или Windows](https://devcolibri.com/как-установить-maven-3-на-ubuntu-или-windows/) -- [Руководство по Maven](https://topjava.ru/blog/apache-maven-osnovy-1) -- Wiki: [Apache Maven](https://ru.wikipedia.org/wiki/Apache_Maven) -- [The Central Repository](http://search.maven.org) -- Дополнительно: - - [Мой Wiki по Maven](https://github.com/JavaOPs/topjava/wiki/Maven) - - [Основы Maven](https://www.youtube.com/watch?v=0uwMKktzixU) - - JavaRush: [Основы Maven](https://javarush.ru/groups/posts/2523-chastjh-4osnovih-maven) - - Инструмент сборки проектов [Maven](https://www.examclouds.com/ru/java/java-core-russian/lesson20) - - [Maven Getting Started Guide](https://maven.apache.org/guides/getting-started/index.html) - - [Видео: Maven vs Gradle vs SBT (Архипов, Борисов, Садогурский)](https://www.youtube.com/watch?v=21qdRgFsTy0) - - [Build Lifecycle](http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) - - [Dependency Mechanism](http://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html) - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. [Как правильно относиться к техзаданию (ТЗ). Полуоткрытый интервал.](https://drive.google.com/file/d/1BpTzjNFjS0TSekCyt_xvt6YoLvuw5KTZ) -- [Типы промежутков](https://ru.wikipedia.org/wiki/Промежуток_(математика)) - -## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW0 - -### ВНИМАНИЕ: НЕ НАДО в репозиторий делать Pull Request со своими решениями! См. видео выше ("Работа с проектом") - -Реализовать метод `UserMealsUtil.filteredByCycles` через циклы (`forEach`): -- должны возвращаться только записи между `startTime` и `endTime` -- поле `UserMealWithExcess.excess` должно показывать, превышает ли сумма калорий за весь день значение `caloriesPerDay` - -Т. е. `UserMealWithExcess` - это запись одной еды, но поле `excess` будет одинаково для всех записей за этот день. - -> - Проверьте результат выполнения ДЗ (можно проверить логику в [http://javaops-demo.ru/topjava](http://javaops-demo.ru/topjava), список еды) -> - Оцените Time complexity алгоритма. Если она больше O(N), например O(N*N) или N*log(N), сделайте O(N). -> **Внимание: внимательно прочитайте про O(N). O - это любой коэффициент, 2*N это тоже O(N).** - -- Java 8 Date and Time API -- Алгоритмы и структуры данных для начинающих: сложность алгоритмов -- [Сложность алгоритмов и Big O Notation](https://threadreaderapp.com/thread/1470666237286010881) -- [Головач: сложность алгоритмов в теме коллекций](https://www.youtube.com/watch?v=Ek9ijOiplNE&feature=youtu.be&t=778) -- Time complexity -- Временная сложность алгоритма -- Вычислительная сложность - -#### ВНИМАНИЕ: варианты Optional делайте в одной ветке в разных методах `UserMealsUtil`. Проще делать, проще проверять - -### Optional (Java 8 Stream API) -``` -Реализовать метод `UserMealsUtil.filteredByStreams` через Java 8 Stream API. -``` -- Видео: Доступно о Java 8 Lambda -- Java 8: Lambda выражения -- Java 8: Потоки -- Pуководство по Java 8 Stream -- [Полное руководство по 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` - - без циклов по другим коллекциям/массивам (к ним также относим методы коллекций `addAll()/removeAll()`) -- через Stream API за 1 проход по исходному списку `meals.stream()` - - нельзя использовать внешние коллекции, не являющиеся частью коллектора - - возможно дополнительные проходы по частям списка, при этом превышение должно считаться один раз для всего подсписка. Те например нельзя разбить список на 2 подсписка с четными и нечетными датами и затем их объединить, с подсчетом превышения для каждого элемента. - -Временная сложность реализации должна быть O(N) (обратите внимание на п. 13 замечаний) -Решение должно быть рабочим в общем случае (должно работать в приложении с многими пользователями, не только при запуске `main`) -Нельзя 2 раза проходить по исходному списку (в том числе по его отфильтрованной или преобразованной копии) - -Ресурсы: -- [Baeldung: Custom Collectors](https://www.baeldung.com/java-8-collectors#Custom) -- [Руководство по Java 8 Stream API: Collector](https://annimon.com/article/2778#collector) -- [Хватит писать циклы! Топ-10 лучших методов для работы с коллекциями из Java 8](https://javarush.ru/groups/posts/524-khvatit-pisatjh-ciklih-top-10-luchshikh-metodov-dlja-rabotih-s-kollekcijami-iz-java8) -- [Понять Java Stream API](https://vc.ru/u/604567-yerlan-akzhanov/194409-ponyat-java-stream-api) - -### Замечания по использованию Stream API: -- Когда встречаешь что-то непривычное, приходится перестраивать мозги. Например, переход с процедурного на ООП-программирование дается непросто. Те, кто не знает шаблонов (и не хотят учить), также их встречают плохо. Хорошая новость в том, что если это принять и начать использовать, то начинаешь получать от этого удовольствие. И тут главное не впасть в другую крайность: - - [Используйте Stream API проще (или не используйте вообще)](https://habrahabr.ru/post/337350/) -- Если вас беспокоит производительность стримов, обязательно прочитайте про оптимизацию - - ["Что? Где? Когда?"](http://optimization.guide/intro.html) - - [Перформанс: что в имени тебе моём?](https://habrahabr.ru/company/jugru/blog/338732/) - - [Performance это праздник](https://habrahabr.ru/post/326242/) - -При использовании Stream API производительность улучшится только на больших задачах, где возможно распараллеливание. -Еще: просто так запустить и померить скорость JVM нельзя (как минимум надо дать прогреться и запустить очень большое число раз). Лучше использовать какие-нибудь бенчмарки, например [JMH](http://tutorials.jenkov.com/java-performance/jmh.html), который мы используем на другом проекте (Mastejava). - -## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания к HW0 -- 1: Код проекта менять можно! Одна из распространенных ошибок как в тестовых заданиях на собеседовании, так и при работе на проекте, что ничего нельзя менять. Конечно, при правках в рабочем проекте обязательно нужно проконсультироваться/проревьюироваться у авторов кода (находятся по истории VCS) -- 2: Наследовать `UserMealWithExcess` от `UserMeal` нельзя, т. к. это разные сущности: Transfer Object и Entity. Мы будем их проходить на 2-м уроке. Это относится и к их зависимости друг от друга. -- 3: Правильная реализация должна быть простой и красивой, можно сделать 2-мя способами: через стримы и через циклы. Сложность должна быть O(N), т. е. без вложенных стримов и циклов. -- 4: При реализации через циклы посмотрите в `Map` на методы `getOrDefault` или `merge` -- 5: **При реализации через `Stream` заменяйте `forEach` оператором `stream.map(..)`** -- 6: Объявляйте переменные непосредственно перед использованием (если возможно - сразу с инициализацией). При объявлении коллекций в качестве типа переменной используйте интерфейс (Map, List, ..) -- 7: Если IDEA предлагает оптимизацию (желтым подчеркивает), например, заменить лямбду на ссылку на метод (method reference), соглашайтесь (Alt+Enter) -- 8: Пользуйтесь форматированием кода в IDEA: `Alt+Ctrl+L` -- 9: Перед check-in (отправкой изменений на GitHub) просматривайте внесенные изменения (Git -> [Log](https://www.jetbrains.com/help/idea/log-tab.html) -> курсор на файл и Ctrl+D): не оставляйте в коде ничего лишнего (закомментированный код, TODO и пр.). Если файл не меняется (например только пробелы или переводы строк), не надо его чекинить, делайте ему `revert` (Git -> Revert / `Ctrl+Alt+Z`). -- 10: `System.out.println` нельзя использовать нигде, кроме как в `main`. Позже введем логирование. -- 11: Результаты, возвращаемые `UserMealsUtil.filteredByStreams`, мы будем использовать [в нашем приложении](http://javaops-demo.ru/topjava) для фильтрации по времени и отображения еды правильным цветом. -- 12: Обращайте внимание на комментарии к вашим коммитам в Git. Они должны быть короткие и информативные (лучше на english) -- 13: Не полагайтесь в решении на то, что список еды будет подаваться отсортированным. Такого условия нет. ------ - -> - ДЗ первого урока будет связано с созданием небольшого [CRUD](https://ru.wikipedia.org/wiki/CRUD)-приложения (в памяти, без базы данных) на JSP и сервлетах -> - основы JavaScript необходимы для понимания проекта, начиная с 8-го занятия - -### Полезные ресурсы -#### HTML, JavaScript, CSS -- [Basic HTML and HTML5](https://learn.freecodecamp.org/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/) -- [Справочник по WEB](https://developer.mozilla.org/ru/) -- [Видео по WEB-технологиям](https://www.youtube.com/user/WebMagistersRu/playlists) -- [Изучение JavaScript в одном видеоуроке за час](https://www.youtube.com/watch?v=QBWWplFkdzw) -- HTML, CSS, JAVASCRIPT, SQL, JQUERY, BOOTSTRAP -- Введение в программирование на JavaScript -- Стандарты кодирования для HTML, CSS и JavaScript’a -- Основы работы с HTML/CSS/JavaScript -- JavaScript - Основы -- Основы JavaScript -- Bootstrap 3 - Основы -- jQuery для начинающих - -#### Java (базовые вещи) -- [Сборник видео "Изучаем Java"](https://www.youtube.com/playlist?list=PLyxk-1FCKqockmP-fXZmHQ7UlYP3qvZRa) -- 1-й урок MasterJava: Многопоточность -- [Основы Java garbage collection](http://web.archive.org/web/20180831013112/https://ggenikus.github.io/blog/2014/05/04/gc) -- Размер Java объектов -- Введение в Java Reflection API -- Структуры данных в картинках -- Обзор java.util.concurrent.* -- Синхронизация потоков -- String literal pool -- Маленькие хитрости Java -- 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/) - -#### Сервлеты -- Как создать Servlet? Полное руководство. -- [Сервлеты](https://metanit.com/java/javaee/4.1.php) - -#### Туториалы по Spring -- [Юрий Ткач: Spring Framework - The Basics](https://www.youtube.com/playlist?list=PL6jg6AGdCNaWF-sUH2QDudBRXo54zuN1t) -- [Java Brains: Spring Framework](https://www.youtube.com/playlist?list=PLC97BDEFDCDD169D7) -- [Тимур Батыршинов: Spring Core - основы фреймворка, ядро](https://www.youtube.com/watch?v=CfHDr-19WWY&list=PL8X2nqRlWfaYYP1-qXjdPKE7bXYkl6aL4) -- [alishev: Spring Framework](https://www.youtube.com/playlist?list=PLAma_mKffTOR5o0WNHnY0mTjKxnCgSXrZ) - -#### JDBC, SQL -- Основы SQL на примере задачи -- Уроки по JDBC -- Learn SQL -- Интуит. Основы SQL -- Try SQL -- Курс "Введение в базы данных" - -#### Разное -- Вопросы по собеседованию, ресурсы для подготовки -- Эффективная работа с кодом в IntelliJ IDEA -- Quizful- тесты онлайн -- Введение в Linux - -#### Книги -- Джошуа Блох: Java. Эффективное программирование. Второе издание -- Гамма, Хелм, Джонсон: Приемы объектно-ориентированного проектирования. Паттерны проектирования -- Редмонд Э.: Семь баз данных за семь недель. Введение в современные базы данных и идеологию NoSQL -- Brian Goetz: Java Concurrency in Practice -- G.L. McDowell: Cracking the Coding Interview +Наиболее востребованные технологии /инструменты / фреймворки Java Enterprise: +Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + plugins. + +- [Вступительное занятие](https://github.com/JavaOPs/topjava) +- [Описание и план проекта](https://github.com/JavaOPs/topjava/blob/master/description.md) +- [Wiki](https://github.com/JavaOPs/topjava/wiki) +- [Wiki Git](https://github.com/JavaOPs/topjava/wiki/Git) +- [Wiki IDEA](https://github.com/JavaOPs/topjava/wiki/IDEA) +- [Демо разрабатываемого приложения](http://topjava.herokuapp.com/) + +#### 28.05: Старт проекта +- Начало проверки [вступительного задания](https://github.com/JavaOPs/topjava#-Домашнее-задание-hw0) + +#### 04.06: 1-е занятие +- Разбор домашнего задания вступительного занятия (вместе с Optional) +- Обзор используемых в проекте технологий. Интеграция ПО +- Maven +- WAR. Веб-контейнер Tomcat. Сервлеты +- Логирование +- Уровни и зависимости логгирования. JMX +- Домашнее задание 1-го занятия (HW1 + Optional) + +### 07.06: Дедлайн подачи заявки на [дипломную программу](https://javaops.ru/view/register/diploma) + +#### 11.06: 2-е занятие +- Разбор домашнего задания HW1 + Optional +- Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava +- Слои приложения. Создание каркаса приложения +- Обзор Spring Framework. Spring Context +- Пояснения к HW2. Обработка Autowired +- Домашнее задание (HW2 + Optional) + +#### 18.06: 3-е занятие +- Разбор домашнего задания HW2 + Optional +- Жизненный цикл Spring контекста +- Тестирование через JUnit +- Spring Test +- Базы данных. Обзор NoSQL и Java persistence solution без ORM +- Настройка Database в IDEA +- Скрипты инициализации базы. Spring Jdbc Template +- Тестирование UserService через AssertJ +- Логирование тестов +- Домашнее задание (HW3 + Optional) + +#### 25.06: 4-е занятие +- Разбор домашнего задания HW3 + Optional +- Методы улучшения качества кода +- Spring: инициализация и популирование DB +- Подмена контекста при тестировании +- ORM. Hibernate. JPA +- Поддержка HSQLDB +- Домашнее задание (HW4 + Optional) +#### Начало выполнения [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md) + +#### 02.07: 5-е занятие +- Обзор JDK 9/11. Миграция Topjava с 1.8 на 11 +- Разбор вопросов +- Разбор домашнего задания HW4 + Optional +- Транзакции +- Профили Maven и Spring +- Пул коннектов +- Spring Data JPA +- Spring кэш +- Домашнее задание (HW5 + Optional) + +#### 09.07: 6-е занятие +- Разбор домашнего задания HW5 + Optional +- Кэш Hibernate +- Spring Web +- JPS, JSTL, internationalization +- Динамическое изменение профиля при запуске +- Конфигурирование Tomcat через maven plugin. Jndi-lookup +- Spring Web MVC +- Spring Internationalization +- Домашнее задание (HW6 + Optional) + +#### Большое ДЗ + выпускной проект + подтягиваем "хвосты". + +#### 23.07: 7-е занятие +- Разбор домашнего задания HW6 + Optional +- Автогенерация DDL по модели +- Тестирование Spring MVC +- Миграция на JUnit 5 +- Принципы REST. REST контроллеры +- Тестирование REST контроллеров. Jackson +- jackson-datatype-hibernate. Тестирование через матчеры +- Тестирование через SoapUi. UTF-8 +- Домашнее задание (HW7 + Optional) + +#### 30.07: 8-е занятие +- Разбор домашнего задания HW7 + Optional +- WebJars. jQuery и JavaScript frameworks +- Bootstrap +- AJAX. Datatables. jQuery +- jQuery notifications plugin +- Добавление Spring Security +- Домашнее задание (HW8 + Optional) + +#### 06.08: 9-е занятие +- Разбор домашнего задания HW8 + Optional +- Spring Binding +- Spring Validation +- Перевод DataTables на Ajax +- Форма login / logout +- Реализация собственного провайдера авторицазии +- Принцип работы Spring Security. Проксирование +- Spring Security Test +- Cookie. Session +- Домашнее задание (HW9 + Optional) + +#### 13.08: 10-е занятие +- Разбор домашнего задания HW10 + Optional +- Кастомизация JSON (@JsonView) и валидации (groups) +- Рефакторинг: jQuery конверторы и группы валидации по умолчанию +- Spring Security Taglib. Method Security Expressions +- Интерсепторы. Редактирование профиля. JSP tag files +- Форма регистрации +- Обработка исключений в Spring +- Encoding password +- Миграция на Spring 5 +- Защита от межсайтовой подделки запросов (CSRF) +- Домашнее задание (HW10) + +#### 20.08: 11-е занятие +- Разбор домашнего задания HW10 + Optional +- Локализация datatables, ошибок валидации +- Защита от XSS (Cross Site Scripting) +- Обработка ошибок 404 (NotFound) +- Доступ к AuthorizedUser +- Ограничение модификации пользователей +- Деплой [приложения в Heroku](http://topjava.herokuapp.com) +- Собеседование. Разработка ПО +- Возможные доработки приложения +- Домашнее задание по проекту: составление резюме + +### 10.09: Дедлайн на сдачу [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md) +### 23.09: Получение дипломов для участников [Дипломной программы](https://javaops.ru/view/register/diploma) diff --git a/ReleaseNotes.md b/ReleaseNotes.md deleted file mode 100644 index 40fef33ddfb3..000000000000 --- a/ReleaseNotes.md +++ /dev/null @@ -1,285 +0,0 @@ -# TopJava Release Notes - -### TopJava 26 -- починили шаблоны запросов на создание в Swagger через [ApiModelProperty](https://www.baeldung.com/spring-swagger-hide-field) -- в новой версии Data JPA `getById`->`getReferenceById` -- мелкие правки - -### TopJava 25 -- поправил `NoHtmlValidator`: `Jsoup.clean().equals` -> `Jsoup.isValid` -- починил `role VARCHAR NOT NULL` -- добавил нового тестового пользователя `guest` - -### TopJava 24 -- migrate to LTS JDK 17 -- add `AbstractBaseEntity.id()` -- fix for `User.registered`: `@Column(..., updatable = false)` -- add `@QueryHints` in `CrudUserRepository` -- add loggin in `RootController` -- fix `@Sql(.., , executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)` for controller tests after service tests -- rename `MATCHER` to `USER_MATCHER/MEAL_MATCHER` -- treat in `ActiveDbProfileResolver` attribute `profiles` - -### TopJava 23 -- migrate to JDK 16 -- в новой spring-data-jpa `getOne` заменили на `getById` -- в UserUtil#prepareToSave убрал проверку пароля на `hasText`. На UI поле проверяется на `@NotBlank` -- `ProfileRestController#register` делаю по правилам REST (POST без "/register") -- css стили `data-...` сделал [low-case через дефисы](https://stackoverflow.com/questions/36176474/548473) -- `TestMatcher` переименовал в `MatcherFactory` -- Для Swagger UI пометил `AuthorizedUser` аннотацией `@ApiIgnore` - -### TopJava 22 - - очистка пароля `AuthorizedUser#userTo` - - заменил `@SafeHtml`, который удалили из `hibernate.validator` на [Jsoup.clean](https://stackoverflow.com/a/68888601/548473) - - перенес запрет на обновление admin/user в `UserService` - - проверку email на уникальность для update с `id=null` в теле запроса сделал на основе анализа `HttpServletRequest.getRequestURI()` - - проверку класса в `classpath` в `Profiles#getActiveDbProfile` делаю на `org.springframework.util.ClassUtils#isPresent` - - удалил `type="text/javascript"` - -### 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` из-за ошибок округления попадает в предыдущий интервал. -Мораль: всегда в тестах проверяйте граничные значения. Добавил этот случай в тестовые данные. -- Изменил `MealRepository.getBetween` (принимаю `@Nullable LocalDate`). Изменились реализации. -- Выделил метод `UserService.prepareAndSave` -- В TO поля сделал `final`, используем `@ConstructorProperties` -- Наконец локализировал описание приложения на страничке входа -- Для полей ввода дат добавил `autocomplete="off"` -- При закрытии модального окна закрываю окно ошибок -- Тесты: - - **Вместо очистки кэшей перед каждым тестом отключаем кэши для всех тестов** - - **Вынес общий код тестирования контроллеров в `AbstractControllerTest`. Код тестов значительно сократился** - - **Сделал типизированный `TestMatchers` для проверки результатов тестов. В классах `UserTestData` и `MealTestData` создаю его инстансы с заданным типом и методикой сравнения.** - - В тестах `delete` и `create` проверяю результат напрямую (не через `getAll`) - - -### TopJava 17 -- Удалил `Impl` из названий репозиториев -- Удалил интерфейсы к сервисам, использую классы -- Добавил `AdminRestController.enable`, вызов через PATCH метод -- Добавил валидацию для jdbc через Bean Validation API -- Перенес работу в UI с профилем из `RootController` в `ProfileUIController` -- `SLF4JBridgeHandler` инициализирую только в профиле `postgres` - -### TopJava 16 -- Выделил общий код реализации хранения в памяти в `InMemoryBaseRepositoryImpl` -- Сделал подтверждение для удаления записей -- Обновились видео 7-го занятия. [Выложил его как пример занятия, некоторые видео открыты](https://github.com/JavaOPs/topjava/blob/master/doc/lesson07.md) -- Сделали валидации дублирования email через `WebDataBinder` и `Validator` - -### TopJava 15 -- Миграция на Servlet API 4.0 / Tomcat 9.x -- [Миграция на JDK11](http://javaops.ru/view/resources/jdk8_11) -- JUnit5 fix: junit-platform-surefire-provider не нужен -- Рефакторинг тестов: - - в `RootControllerTest.testUsers` для проверки используем `AssertionMatcher` адаптер - - вместо `content().json()` от `jsonassert` десериализуем json и используем сравнения через `AssertJ` -- В javascript место глабальных переменных и одинаковой функции обновления таблицы задаю их в объекте контекст, который передаю в `makeEditable()` как параметр -- Починил `back` в браузере после логина. Кнопки входа и регистрации отображаю только для `isAnonymous()` - -### TopJava 14 -- [Миграция на JUnit 5](http://javaops.ru/view/resources/junit5) -- Для измерения времени в тестах использую [Spring StopWatch](https://www.logicbig.com/how-to/code-snippets/jcode-spring-framework-stopwatch.html) -- `SimpleJdbcInsert` и `NamedParameterJdbcTemplate` конструируются (и берут настройки) из `jdbcTemplate` -- `AuthorizedUser` зарефакторился в `SecurityUtil` -- В javascript [заменил `var` на `let/const`](https://learn.javascript.ru/let-const). [Поддержка 95% браузеров](https://caniuse.com/#feat=const) -- Подправил UI фильтрации и заголовка страниц, добавилась кнопка `Cancel` в профиль -- Починил [баг в FireFox](https://bugzilla.mozilla.org/show_bug.cgi?id=884693): пустой ответ по ajax -- Сделал вход в приложение при нажании кнопок `Зайти как ...` -- Добавил регистрацию пользователя по REST -- Преименовал js файлы согласно [javascript filename naming convention](https://stackoverflow.com/questions/7273316/what-is-the-javascript-filename-naming-convention) -- Сделал проверку startTime/endTime на фильтре времени (после обновления datetimepicker до 2.5.20) - -### TopJava 13 -- [Миграция на Botstrap 4](https://getbootstrap.com/docs/4.1/migration/) -- Добавил [Responsive behaviors](https://getbootstrap.com/docs/4.1/components/navbar/#responsive-behaviors) - при уменшении ширины экрана навигация сворачивается в кнопку -- Для отображения цвета еды и выключенного юзера использую [data-* атрибуты](https://developer.mozilla.org/ru/docs/Web/Guide/HTML/Using_data_attributes) -- В `inputField.tag` передаю как параметр код для локализации label, а в `i18n.jsp` передаю как параметр `page`. См. [JSP include action with parameter example](https://beginnersbook.com/2013/12/jsp-include-with-parameter-example) - -### TopJava 12 -- [Миграция на Spring 5](http://javaops.ru/view/resources/spring5) -- обновил версии: Ehcache 3.x, datatables, datetimepicker -- добавил видео решений HW0 с одним проходом -- поправил видео [Обзор Spring Framework. Spring Context](https://drive.google.com/file/d/1fBSLGEbc7YXBbmr_EwEHltCWNW_pUmIH). Дописал про Constructor injection. -- заменил видео про тетсирование сервисов. Вместо самодельных матчеров стали использовать [AssertJ](http://joel-costigliola.github.io/assertj/index.html). Видео [Тестирование UserService через AssertJ](https://drive.google.com/open?id=1SPMkWMYPvpk9i0TA7ioa-9Sn1EGBtClD), время 1:53 -- сделал [видео с jQuery конвертерами и дефолтными группами валидации при сохранении в базу](https://drive.google.com/open?id=1tOMOdmaP5OQ7iynwC77bdXSs-13Ommax) -- сделал [видео с новым `DelegatingPasswordEncoder` и Json READ/WRITE access](https://drive.google.com/file/d/1XZXvOThinzPw4EhigAUdo8-MWT_g8wOt/view?usp=sharing) -- убрал `AccessType.PROPERTY` для `AbstractBaseEntity.id` (см. [fixed HHH-3718](https://hibernate.atlassian.net/browse/HHH-3718)) -- удалил `PasswordUtil`, возвращаю статус `NO_CONTENT` для REST delete, убрал группы валидации в `UserTo` -- заменил в jQuery [success на done](https://stackoverflow.com/a/22213543/548473) -- вместо `lang.jsp` сделал общий `bodyHeader.jsp` - -### TopJava 11 -- добавил - - доп. решение HW1 через одним return и O(N) - - раскрасил лог ([Logback layouts coloring](https://logback.qos.ch/manual/layouts.html#coloring)) -- рефакторинг - - починил коммит формы по cancel (`history.back()`) в FireFox - - заменил неработающий DependencyCi на [VersionEye](https://www.versioneye.com/) c проверкой зависимостей на uptodate - - починил `CrudUserRepository.getWithMeals()` через `@EntityGraph`. С неколькими ролями (у админа) еда дублируется - - починил тесты контроллеров с профилем JDBC (`JpaUtil` отсутствует в контексте JDBC) - - переименовал `meal.jsp/user.jsp` в `mealForm.jsp/userForm.jsp` - - в `InMemoryMealRepositoryImpl.save()` сделал update атомарным - - переименовал методы сервисов `save` в `create` - - переименовал и cделал классы `BaseEntity` и `NamedEntity` абстрактными - - обновил Noty и API с ним до 3.1.0. Добавил glyphicon в сообщения Noty - - заменил `MATCHER_WITH_EXCEED` на валидацию через [JSONassert](https://github.com/skyscreamer/JSONassert). - - поменял Deprecated валидаторы `org.hibernate.validator.constraints` на `javax.validation.constraints` - - убрал пароль из результатов REST через [@JsonProperty READ_ONLY / WRITE_ONLY](https://stackoverflow.com/questions/12505141/only-using-jsonignore-during-serialization-but-not-deserialization/12505165#12505165). Тесты на REST пришлось починить добавлением добавлением в JSON пароля как дополнительного параметра (`JsonUtil.writeWithExtraProps`) - - **убрал JSON View и сделал преобразование времени на UI с помощью [jQuery converters](http://api.jquery.com/jQuery.ajax/#using-converters)** - - **поменял [группу валидации по умолчанию при сохранении через JPA](https://stackoverflow.com/questions/16930623/16930663#16930663).** Теперь - все валидаторы в модели работаю по умолчанию (`groups` не требуется). - - Добавил в `ErrorInfo` тип ошибки `ErrorType` + i18n. - -- правки - - переименовал `ModelMatcher` в `BeanMatcher` и починил: можно сравнивать только упорядоченные коллекции (List) - - поменял зависимость `org.hibernate:hibernate-validator` на `org.hibernate.validator:hibernate-validator` (warning при сборке) - -### TopJava 10 -- добавил - - доступ к AuthorizedUser через [`@AuthenticationPrincipal`](http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-authentication-principal) и [authentication Tag](http://docs.spring.io/spring-security/site/docs/current/reference/html/taglibs.html#the-authentication-tag) - - [Обработку 404 NotFound](https://stackoverflow.com/questions/18322279/spring-mvc-spring-security-and-error-handling) - - локализацию ошибок валидации - - проверки json в тестах через [JSONassert](https://github.com/skyscreamer/JSONassert) и [через jsonPath](https://www.petrikainulainen.net/programming/spring-framework/integration-testing-of-spring-mvc-applications-write-clean-assertions-with-jsonpath/) - - [логирование от Postgres Driver](http://stackoverflow.com/a/43242620/548473) - - в `.travis.yml` [сборку только ветки master](https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches) - - [защиту от кэширование ajax запросов в IE](https://stackoverflow.com/a/4303862/548473) - - обработку запрета модификации системный юзеров через универсальный `ApplicationException` - - рефакторинг - - сделал `@EntityGraph` через `attributePaths` - - реализаовал обработку дублирования `user.email` и `meal.dateTime` через [Controller Based Exception Handling](https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc#controller-based-exception-handling) - - поменял отключение транзакционности в тестах через `@Transactional(propagation = Propagation.NEVER)` - - сделал выбор в сервлете через switch - - [все логгирование сделал через {} форматирование](http://stackoverflow.com/questions/10555409/logger-slf4j-advantages-of-formatting-with-instead-of-string-concatenation) и поправил его в контроллерах (поле проверки id) - - [перешел на конструктор DI](http://stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it) - - в `ModelMatcher` переименовал `Comparator` -> `Equality` - - [заинлайнил все лямбды](http://stackoverflow.com/questions/19718353/is-repeatedly-instantiating-an-anonymous-class-wasteful) (компараторы, ModelMatcher.equality) - - поменялась реализация `JdbcUserRepositoryImpl.getAll()` - - на UI кнопки в таблице заменились на линки, поправил сообщения локализации - - [сделал кастомизацию JSON (@JsonView) и валидацию (groups)](https://drive.google.com/file/d/0B9Ye2auQ_NsFRTFsTjVHR2dXczA) для данных еды, отдаваемых на UI - - в `JdbcUserRepositoryImpl` поменял `MapSqlParameterSource` на `BeanPropertySqlParameterSource` -- удалил - - зависимость `javax.transaction.jta` (уже не нужна) - - `${spring.version}` в `pom.xml` зависимостях (уже есть в `spring-framework-bom`) - - distinct из запроса Hibernate на пользователей с ролями. [Оптимизация запроса distinct: 15.16.2](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct) - - лишние `
` тэги (`shadow` и `view-box`) - -### TopJava 9 -- добавил - - выбор профиля базы через `ActiveProfilesResolver`/`AllActiveProfileResolver` на основе драйвера базы в classpath - - видео Cascade. Auto generate DDL. - - проверку на правильность id в Ajax/Rest контроллерах (treat IDs in REST body) - - тесты на валидацию входных значений контроллеров и зависимость на имплементацию - - Bootstrap Glyphicons -- рефакторинг - - переименовал `TimeUtil` в `DateTimeUtil` - - переименовал `ExceptionUtil` в `ValidationUtil` - - заменил валидацию `@NotEmpty` на `@NotBlank` - - заменил `CascadeType.REMOVE` на `@OnDelete` - - изменил `JdbcUserRepositoryImpl.getAll()` - - обновил jQuery до 3.x, исключил из зависимостей webjars ненужные jQuery - - cделал загрузку скриптов асинхронной - - фильтр еды сделал в [Bootstrap Panels](http://getbootstrap.com/components/#panels) - - вместо `Persistable` ввел интерфейс `HasId` и наследую от него как Entity, так и TO - - сделал универсальную обработку исключений дублирования email и dateTime - -### TopJava 8 -- добавил: - - [защиту от XSS (Cross-Site Scripting)](http://stackoverflow.com/a/40644276/548473) - - интеграцию с Dependency Ci и Travis Ci - - локализацию календаря - - сводку по результатам тестов - - примеры запросов curl в `config/curl.md` - - DataTables/Bootstrap 3 integration - - тесты на профиль деплоя Heroku (общее количество JUnit тестов стало 102) -- удалил зависимость `jul-to-slf4j` -- рефакторинг - - переименовал все классы `UserMeal**` в `Meal**`, JSP - - переименовал `LoggedUser` в `AuthorizedUser` - - починил работа с PK Hibernate в случае ленивой загрузки (баг HHH-3718) - - поменял в `BaseEntity` `equals/hashCode/implements Persistable` - - в `InMemoryMealRepositoryImpl` выделил метод `getAllStream` - - перенес проверки пердусловий `Assert` из `InMemory` репозиториев в сервисы - - переименовал классы _Proxy*_ на более адекватные _Crud*_ - - поменял реализацию `JpaMealRepositoryImpl.get`, добавил в JPA модель `@BatchSize` - - вместо `@RequestMapping` ввел Spring 4.3 аннотации `@Get/Post/...Mapping` - - поменял авторизацию в тестах не-REST контроллеров - - перенес вызовы `UserUtil.prepareToSave` из `AbstractUserController` в `UserServiceImpl` - - зарефакторил обработку ошибок (`ExceptionInfoHandler`) - -### TopJava 7 -- добавил: - - [JPA 2.1 EntityGraph](https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs002.htm) - - [Jackson @JsonView](https://habrahabr.ru/post/307392/) - - валидацию объектов REST - - [i18n в JavaScript](http://stackoverflow.com/a/6242840/548473) - - проверку предусловий и видео Методы улучшения качества кода - - интеграцию с проверкой кода в Codacy - - [сравнение вермени исполнения запросов при различных meals индексах](https://drive.google.com/open?id=0B9Ye2auQ_NsFX3RLcnJCWmQ2Y0U) -- tomcat7-maven-plugin плагин перключили на Tomcat 8 (cargo-maven2-plugin) -- рефакторинг - - обработка ошибок сделал с array - - матчеров тестирования (сделал автоматические обертки и сравнение на основе передаваемого компаратора) - - вынес форматирование даты в `functions.tld` - -### TopJava 3-6 -- добавил - - [выпускной проект](https://drive.google.com/open?id=0B9Ye2auQ_NsFcG83dEVDVTVMamc) - - в таблицу meals составной индекс - - константы `Profiles.ACTIVE_DB`, `Profiles.DB_IMPLEMENTATION` - - проверки и тесты на `NotFound` для `UserMealService.getWithUser` и `UserService.getWithMeals` - - в MockMvc фильтр CharacterEncodingFilter - - защиту от межсайтовой подделки запроса, видео Межсайтовая подделка запроса (CSRF) - - ограничение на диапазон дат для фильтра еды -- рефакторинг - - UserMealsUtil, ProfileRestController, компараторов в репозитоии - - `LoggedUser` отнаследовал от `org.springframework.security.core.userdetails.User` - - переименовал `DbTest` в `AbstractServiceTest` и перенес сюда `@ActiveProfiles` - - сделал выполнение скриптов в тестах через аннотацию `@Sql` - - вместо использования id и селектора сделал обработчик `onclick` - - изменил формат ввода даты в форме без 'T' -- убрал - - `LoggerWrapper` - - Dandelion обертку к datatables -- обновил - - Hibernate до 5.x и Hibernate Validator, добавились новые зависимости и `jackson-datatype-hibernate5` - - datatables API (1.10) - - Postgres драйвер. Новый драйвер поддерживает Java 8 Time API, разделил реализацию JdbcMealRepositoryImpl на Java8 (Postgresql) и Timestamp (HSQL) diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml new file mode 100644 index 000000000000..7cba42ef2387 --- /dev/null +++ b/config/Topjava-soapui-project.xml @@ -0,0 +1,590 @@ + + + + + + + + http://localhost:8080 + + + + + + + + + text/html;charset=utf-8 + 500 401 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/admin/users + + user@yandex.ru + password + No Authorization + Basic + No Authorization + + + + + + + + + + + application/json + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 201 + + user:Response + + + application/json;charset=UTF-8 + 201 + + user:Response + + + + <xml-fragment/> + + http://localhost:8080 + {"name": "New2", + "email": "new2@yandex.ru", + "password": "passwordNew", + "roles": ["USER"] +} + + http://localhost/topjava/rest/admin/users + + No Authorization + Basic + No Authorization + + + + + + + + + + + + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + ns:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/admin/users/100000 + + No Authorization + Basic + No Authorization + + + + + + + + + + + text/html;charset=utf-8 + 405 500 + + html + + + application/json + + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + <xml-fragment/> + + + http://localhost:8080 + {"name": "UserUpdated", + "email": "user@yandex.ru", + "password": "passwordNew", + "roles": ["USER"] +} + + http://localhost/topjava/rest/admin/users/100000 + + Basic + Basic + Global HTTP Settings + + + + + + + + + + + + + + + text/html;charset=utf-8 + 500 + + html + + + application/json + 200 + + ns:Response + + + application/json;charset=UTF-8 + 200 + + prof:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile + + Basic + Basic + Global HTTP Settings + + + + + + + + + + + application/json + + + + text/html;charset=utf-8 + 500 405 + + html + + + application/json + 201 + + user:Response + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + + + 200 + + data + + 200data + + <xml-fragment/> + + http://localhost:8080 + {"name": "New777", + "email": "new777@yandex.ru", + "password": "passwordNew", + "roles": ["USER"] +} + + http://localhost/topjava/rest/profile + + No Authorization + Basic + No Authorization + + + + + + + + + + + + 200 + + data + + + text/html;charset=utf-8 + 500 + + html + + + application/json + + + + + 200 + + data + + 204data + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile + + No Authorization + Basic + No Authorization + + + + + + + + + + + + + + + + 0 + + data + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals + + No Authorization + + + + + + + + + + + application/json + + + + application/json;charset=UTF-8 + 201 + + meal:Response + + + + <xml-fragment/> + + + http://localhost:8080 + { +"dateTime": "2020-02-01T10:00", +"description": "Новый завтрак", +"calories": 777 +} + + http://localhost/topjava/rest/profile/meals + + No Authorization + + + + + + + + + + + + + + + application/json;charset=UTF-8 + 200 + + ns:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals/100002 + + No Authorization + + + + + + + + + + + + + 200 + + data + + + application/json + + + + + 200 + + data + + 200data + + <xml-fragment/> + + UTF-8 + http://localhost:8080 + { +"id": 100002, +"dateTime": "2020-01-30T10:00", +"description": "Обновленный завтрак", +"calories": 500 +} + + http://localhost/topjava/rest/profile/meals/100002 + + No Authorization + + + + + + + + + + + + + startDate + + QUERY + + + + + startTime + + QUERY + + + + + endDate + + QUERY + + + + + endTime + + QUERY + + + + + + + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals/filter + + No Authorization + + + + + + + + + + + startDate + startTime + endDate + endTime + + + + + + + + + + + \ No newline at end of file diff --git a/config/api-docs b/config/api-docs new file mode 100644 index 000000000000..56360a285e99 --- /dev/null +++ b/config/api-docs @@ -0,0 +1,13 @@ + +{ + "apiVersion":"1.0", + "swaggerVersion":"1.2", + "basePath":"http://localhost:8080", + "apis":[ + { + "path":"/httplocalhost8080-api-docs.json" + } + ], + "info":{ + } +} \ No newline at end of file diff --git a/config/curl.md b/config/curl.md new file mode 100644 index 000000000000..3128ccbf451d --- /dev/null +++ b/config/curl.md @@ -0,0 +1,38 @@ +### curl samples (application deployed at application context `topjava`). +> For windows use `Git Bash` + +#### get All Users +`curl -s http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` + +#### get Users 100001 +`curl -s http://localhost:8080/topjava/rest/admin/users/100001 --user admin@gmail.com:admin` + +#### register Users +`curl -s -i -X POST -d '{"name":"New User","email":"test@mail.ru","password":"test-password"}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/register` + +#### get Profile +`curl -s http://localhost:8080/topjava/rest/profile --user test@mail.ru:test-password` + +#### get All Meals +`curl -s http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` + +#### get Meals 100003 +`curl -s http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` + +#### filter Meals +`curl -s "http://localhost:8080/topjava/rest/profile/meals/filter?startDate=2020-01-30&startTime=07:00:00&endDate=2020-01-31&endTime=11:00:00" --user user@yandex.ru:password` + +#### get Meals not found +`curl -s -v http://localhost:8080/topjava/rest/profile/meals/100008 --user user@yandex.ru:password` + +#### delete Meals +`curl -s -X DELETE http://localhost:8080/topjava/rest/profile/meals/100002 --user user@yandex.ru:password` + +#### create Meals +`curl -s -X POST -d '{"dateTime":"2020-02-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` + +#### update Meals +`curl -s -X PUT -d '{"dateTime":"2020-01-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` + +#### validate with Error +`curl -s -X POST -d '{}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` diff --git a/config/httplocalhost8080-api-docs.json b/config/httplocalhost8080-api-docs.json new file mode 100644 index 000000000000..5f6710199465 --- /dev/null +++ b/config/httplocalhost8080-api-docs.json @@ -0,0 +1,208 @@ + +{ + "swaggerVersion":"1.2", + "apiVersion":"1.0", + "basePath":"http://localhost:8080", + "resourcePath":"", + "apis":[ + { + "path":"/topjava/rest/admin/users", + "description":"", + "operations":[ + { + "nickname":"", + "method":"GET", + "summary":"", + "type":"string", + "produces":[ + "application/json", + "application/json;charset=UTF-8" + ], + "responseMessages":[ + { + "code":500, + "message":"" + }, + { + "code":401, + "message":"" + }, + { + "code":200, + "message":"" + } + ] + }, + { + "nickname":"", + "method":"POST", + "summary":"", + "type":"string", + "produces":[ + "application/json", + "application/json;charset=UTF-8" + ], + "parameters":[ + { + "name":"body", + "paramType":"body", + "allowMultiple":false, + "description":"Request body", + "required":true, + "type":"string" + } + ], + "responseMessages":[ + { + "code":500, + "message":"" + }, + { + "code":201, + "message":"" + } + ] + } + ] + }, + { + "path":"/topjava/rest/admin/users/100000", + "description":"", + "operations":[ + { + "nickname":"", + "method":"GET", + "summary":"", + "type":"string", + "produces":[ + "application/json", + "application/json;charset=UTF-8" + ], + "responseMessages":[ + { + "code":500, + "message":"" + }, + { + "code":200, + "message":"" + } + ] + }, + { + "nickname":"", + "method":"PUT", + "summary":"", + "type":"string", + "parameters":[ + { + "name":"body", + "paramType":"body", + "allowMultiple":false, + "description":"Request body", + "required":true, + "type":"string" + } + ], + "responseMessages":[ + { + "code":405, + "message":"" + }, + { + "code":500, + "message":"" + }, + { + "code":200, + "message":"" + } + ] + } + ] + }, + { + "path":"/topjava/rest/profile", + "description":"", + "operations":[ + { + "nickname":"", + "method":"GET", + "summary":"", + "type":"string", + "produces":[ + "application/json", + "application/json;charset=UTF-8" + ], + "responseMessages":[ + { + "code":500, + "message":"" + }, + { + "code":200, + "message":"" + } + ] + }, + { + "nickname":"", + "method":"PUT", + "summary":"", + "type":"string", + "produces":[ + "application/json" + ], + "parameters":[ + { + "name":"body", + "paramType":"body", + "allowMultiple":false, + "description":"Request body", + "required":true, + "type":"string" + } + ], + "responseMessages":[ + { + "code":500, + "message":"" + }, + { + "code":405, + "message":"" + }, + { + "code":201, + "message":"" + }, + { + "code":200, + "message":"" + } + ] + }, + { + "nickname":"", + "method":"DELETE", + "summary":"", + "type":"string", + "responseMessages":[ + { + "code":200, + "message":"" + }, + { + "code":500, + "message":"" + }, + { + "code":204, + "message":"" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/config/messages/app.properties b/config/messages/app.properties new file mode 100644 index 000000000000..f2bd30343f85 --- /dev/null +++ b/config/messages/app.properties @@ -0,0 +1,46 @@ +app.title=Calories management +app.stackTitle=Application stack: +app.description=Java Enterprise project with registration/authorization and role-based access rights (USER, ADMIN). \ +Admin could create/edit/delete users, users - manage your profile and data (meals) via UI (AJAX) and REST with basic authorization. \ +Meals could be filtered by date and time. Meal record color depends on daily calories sum exceeding "Daily calorie limit" (editable user's profile paramets). \ +All REST interface covered with JUnit tests by Spring MVC Test Spring Security Test. +app.footer=Spring 5/JPA Enterprise (Topjava) internship application +app.login=Login as +app.profile=profile +app.register=Registration +app.registered=You are registered. Please Sign in. + +user.title=Users +user.edit=Edit user +user.add=Add user +user.name=Name +user.email=Email +user.roles=Roles +user.active=Active +user.registered=Registered +user.password=Password +user.caloriesPerDay=Daily calorie limit + +meal.title=Meals +meal.edit=Edit meal +meal.add=Add meal +meal.filter=Filter +meal.startDate=From date (inclusive) +meal.endDate=To date (inclusive) +meal.startTime=From time (inclusive) +meal.endTime=To time (exclusive) +meal.description=Description +meal.dateTime=Date/Time +meal.calories=Calories + +common.add=Add +common.select=Select +common.deleted=Record deleted +common.saved=Record saved +common.enabled=Record enabled +common.disabled=Record disabled +common.errorStatus=Error status +common.appError=Application error +common.confirm=Are you sure? +common.save=Save +common.cancel=Cancel \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties new file mode 100644 index 000000000000..be85ad30ed7c --- /dev/null +++ b/config/messages/app_ru.properties @@ -0,0 +1,46 @@ +app.title=Подсчет калорий +app.stackTitle=Стек технологий: +app.description=Java Enterprise проект с регистрацией/авторизацией и правами доступа на основе ролей (USER, ADMIN). \ +Администратор может создавать/редактировать/удалять пользователей, а пользователи - управлять своим профилем и данными (едой) через UI (по AJAX) и по REST интерфейсу с базовой авторизацией. \ +Возможна фильтрация еды по датам и времени. Цвет записи таблицы еды зависит от того, превышает ли сумма калорий за день норму (редактируемый параметр в профиле пользователя). \ +Весь REST интерфейс покрывается JUnit тестами, используя Spring MVC Test и Spring Security Test. +app.footer=Приложение стажировки Spring 5/JPA Enterprise (Topjava) +app.login=Зайти как +app.profile=профиль +app.register=Регистрация +app.registered=Вы зарегистрированы. Введите ваш логин/пароль. + +user.title=Пользователи +user.edit=Редактировать пользователя +user.add=Добавить пользователя +user.name=Имя +user.email=Почта +user.roles=Роли +user.active=Активный +user.registered=Зарегистрирован +user.password=Пароль +user.caloriesPerDay=Норма калорий в день + +meal.title=Моя еда +meal.edit=Редактировать еду +meal.add=Добавить еду +meal.filter=Отфильтровать +meal.startDate=От даты (включая) +meal.endDate=До даты (включая) +meal.startTime=От времени (включая) +meal.endTime=До времени (исключая) +meal.description=Описание +meal.dateTime=Дата/Время +meal.calories=Калории + +common.add=Добавить +common.select=Выбрать +common.deleted=Запись удалена +common.saved=Запись сохранена +common.enabled=Запись активирована +common.disabled=Запись деактивирована +common.errorStatus=Статус ошибки +common.appError=Ошибка приложения +common.confirm=Вы уверены? +common.save=Сохранить +common.cancel=Отменить \ No newline at end of file diff --git a/cv.md b/cv.md deleted file mode 100644 index 2f01659fea12..000000000000 --- a/cv.md +++ /dev/null @@ -1,132 +0,0 @@ -## Составление резюме, подготовка к интервью, поиск работы - -![cv](https://cloud.githubusercontent.com/assets/13649199/10877471/93ea86b8-8157-11e5-9bfa-95e3fba75c58.jpg) - -- Научиться программировать сложнее, чем кажется -- [Собеседование. Разработка ПО. Вопросы.](https://drive.google.com/file/d/0B9Ye2auQ_NsFQVc2WUdCR0xvLWM/view?usp=sharing&resourcekey=0-HaWoRxoyboMSKjg5P2I1cQ) -- [Набор ссылок для тренировки и прохождения интервью](https://github.com/andreis/interview) -- [Полезные советы Jun-ам](https://github.com/JavaOPs) - -### Составление резюме: -- [VisualCV: create resume in minutes](https://www.visualcv.com/) -- Выбрать шаблон для резюме -- [GitHub Pages](https://pages.github.com/), Resume template -- Как продать свое резюме в 2 раза дороже -- Как правильно составить резюме -- Резюме программистов. Часть 1 (плохие) -- Резюме программистов. Часть 2 (хорошие) -- Как составить резюме на английском -- ОФОРМЛЕНИЕ IT-РЕЗЮМЕ для USA - -### Наши истории (делимся опытом и успехом) - -### Тесты/задачи онлайн: -- [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 -- Quizful- тесты онлайн -- LeetCode Online Judge -- Sphere online judge -- Codility programmers lessons -- Hackerrank practice coding -- [start.interviewing.io](https://start.interviewing.io/) - -## [Тестовое собеседование, самые спрашиваемые темы](http://javaops.ru/interview/test.html) - -### Интервью: -- [Собеседования для бэкендеров: как готовиться, тренироваться и не облажаться](https://habr.com/ru/companies/getmatch/articles/744760/) -- [10 неочевидных фактов об ИТ-собеседованиях](https://proglib.io/p/molchanie-intervyuera-ili-10-neochevidnyh-faktov-ob-it-sobesedovaniyah-2022-06-14) -- Михаил Портнов. Собеседование на работу: как продать себя грамотно -- Михаил Портнов. Какие вопросы мы задаем на собеседовании? -- Михаил Портнов. Собеседование на работу: жизненный путь -- [Лёша Корепанов. Признаки плохих компаний для программиста](https://www.youtube.com/watch?v=Sj-WSWr-n7U) -- [Лёша Корепанов. Как отвечать на вопросы, которые ты не знаешь. Техническое интервью для программиста](https://www.youtube.com/watch?v=Beoh3tfgPEk) -- Канал: Резюме, поиск работы, интервью -- Яков Файн: Как стать профессиональным Java разработчиком -- Ответы на вопросы на собеседовании Junior Java Developer -- Список вопросов с ответами для собеседования по Java -- Сборка по вопросам на интервью -- Сборка вопросов-ответов от JavaStudy -- [Видео технических интервью от Максима Добрынина](https://www.youtube.com/playlist?list=PLxqzxxW1gWwJvVK11R_lJKAlP_9m3Gu2H) -- [Вопросы по классам коллекциям от JavaRush-1](http://info.javarush.ru/translation/2013/10/08/Часто-задаваемые-на-собеседованиях-вопросы-по-классам-коллекциям-в-Java-Часть-1-.html) -- [Вопросы по классам коллекциям от JavaRush-2](http://info.javarush.ru/translation/2013/10/08/Часто-задаваемые-на-собеседованиях-вопросы-по-классам-коллекциям-в-Java-Часть-2-.html) -- Тест на знание SQL -- Вопросы на собеседовании Java Junior Developer -- Java вопросы с собеседований на Android -- Сборка вопросов от JavaRush -> про clone и finalize объязательно прочтите Джошуа Блох: Java. Эффективное программирование (второе издание) - -- Cracking the Coding Interview -> Особенно обратите внимание на раздел: Часть VIII. Вопросы собеседования - - -### От себя: -- email, skype - очень желательно, чтобы по ним вы были узнаваемы. Заведите рабочие, если не так. -- написать ВЕСЬ IT опыт (исключая опыт пользователя: Windows, MS Word, Photophop, Yandex disk, Google docs, ..): технологии, какие задачи решали (конкретные), какие инструменты использовали, VCS, DB, инструменты сборки, ... включая опыт в ВУЗе. -- на English иметь желательно. Если вакансия опублинована на Englsih - шлите на нем. Часто могут на нем попросить, если работодатель иностранный. -- удобно иметь резюме где-то в Интернете (hh, linkedin, google doc, чтобы им было удобно делиться). - -### Позиционирование проекта TopJava: -- Обязательно убери из резюме **любое упоминание Junior**. Количество обращений возрастет на порядок. Ссылку на стажировку можно поставить: http://javaops.ru/view/topjava (в linkedin: https://www.linkedin.com/company/java-online-projects). -- После завершения проекта ты освоишь все заявленные в нем технологии - вставь их в квалификацию (включая Java 8 Stream and Time API). -- В разделе опыт работы (если нет коммерческого опыта) вставь: - - Участие в разработке Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей - на стеке Maven/ Spring MVC/ Security/ REST(Jackson)/ Java 8 Stream API: - - реализация сохранения в базы Postgres и HSQLDB на основе Spring JBDC, JPA(Hibernate) и Spring-Data-JPA - - реализация и тестирование REST и AJAX контроллеров - - реализация клиента на Bootstrap (css/js), DataTables, jQuery + plugins. - - собственная доработка проекта - -- Делай упор не на обучение, а на **участие в проекте**. Выполнение домашних заданий это полноценное участие с написанием функционала по всем пройденным технологиям. На собеседовании смотрят не на то, что ты заканчивал, а на опыт и знания. - -### В процессе обучения -- Если рассматриваешь предложения по работе, подними в своем профиле этот флаг и обязательно заполни ссылку на резюме. Обновления нашей базы выпускников смотрят уже более 125 партнеров по трудоустройству (компании и индивидуальные рекрутеры). Проверь содержание "Информация для HR": по нему принимают решение, открывать резюме или нет. - -- Вступайте в нашу группу участников Slack: каналы помощи с Java, отзывы о работодателях, обсуждение тестовых заданий, вакансии, цены на рынке труда, IT события, интересные видео и многое другое. - -- Подпишитесь на рассылку вакансий под себя - -### После прохождения испытательного срока жду твою [историю успеха](http://javaops.ru/view/story) - -### Основные сайты поиска работы: -- [Актуальная подборка Junior вакансий от CodeReview](https://jobs.yourcodereview.com?utm_source=partner&utm_medium=javaops&utm_campaign=landing) -- Яндекс агрегатор -- HH -- LinkedIn -- ХабрКарьера -- [headz.io](https://app.headz.io/candidates/new) -- djinni.co (более актуально для Украины) - -[Как изучать Java. Подборка от JavaRush](https://javarush.ru/groups/posts/3538-v-zakladki-kak-izuchatjh-java-boljhshaja-podborka-po-planu-obuchenija-instrumentam-i-poiskam-mo) -

Как выжить на испытательном сроке

- -- Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В - процессе формулирования очень часто приходит ее решение. -- Учись инвестигировать проблему. Внимательное чтение логов и умение дебажить - основные навыки - разработчика. В логах надо читать верх самого нижнего эксепшена - там причина всей портянки. -- Грамотно уделяй время каждой проблеме. Две крайности: сразу бросаться за помощью и - биться над проблемой часами. - Пробуй решить ее сам и, в зависимости от проблемы, выделяй на это разумное время. -- Не бросайся сразу писать код, поищи в проекте схожий функционал! И далее - минимум своих подходов, если хотите внести что-то свое - обязательно спросите! -Проект должен быть однотипным по максимуму во всех мелочах: - описание, пакеты, имена классов и методов и даже переменных, реализация, хелп. -Обычно можно найти сходный функционал, взять его себе и делать в нем изменения под свою реализацию. -- Если тебе что-то объясняют по проекту - обязательно записывай. -- Когда получаешь задачу - уточни все очень подробно. Если задач несколько - обязательно выясни приоритеты! -- Получай в процессе решения обратную связь - в том ли направлении ты идешь -- Не игнорируй совместные ланчи (курилки) -- Готовься к стендапам/летучкам. Задавай на них вменяемые вопросы. Выказывай заинтересованность -- Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи. -- [**5 вещей, которые разработчик должен сделать прежде чем попросить о помощи**](https://techrocks.ru/2018/07/16/5-things-a-developer-should-do-before-asking-for-help/) -- [**Советы новичкам**](https://blog.csssr.com/ru/article/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) -- Видео [Junior и испытательный срок на первой работе](https://www.youtube.com/watch?v=GsGlsCbok-c) -- Типичные ошибки начинающих программистов от 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) -- [От джуна к миддлу: практические советы](https://tproger.ru/articles/ot-dzhuna-k-middlu-prakticheskie-sovety) -## [Отзывы по стажировке TopJava](https://vk.com/topic-74381644_30447246) diff --git a/description.md b/description.md deleted file mode 100644 index aa98eb24fea0..000000000000 --- a/description.md +++ /dev/null @@ -1,79 +0,0 @@ -#### Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css, js), DataTables, jQuery + plugins, Java 8 Stream and Time API и сохранением в базах данных PostgreSQL и HSQLDB. - -- Основное внимание будет уделяться способам решения многочисленных проблем разработки в Spring/JPA, а также структурному (красивому и надежному) java-кодированию и архитектуре приложения. -- Каждая итерация проекта закрепляется домашним заданием по реализации схожей функциональности. Следующее занятие начинается с разбора домашних заданий. -- Большое внимание уделяется тестированию кода: в проекте более 100 JUnit-тестов. -- Несмотря на относительно небольшой размер, приложение разрабатывается с нуля как большой проект: например, используем кэш 2-го уровня Hibernate, настраиваем Jackson для работы с ленивой загрузкой Hibernate, делаем конвертеры для типов LocalDateTime (Java 8 time API). -- Разбираются архитектурные паттерны: слои приложения и как правильно разбивать логику по слоям, когда нужно применять Data Transfer Object. То есть на выходе получается не учебный проект, а хорошо масштабируемый шаблон для большого проекта на всех пройденных технологиях. -- Большое внимание уделяется деталям: популяция базы данных, использование транзакционности, тесты сервисов и REST-контроллеров, настройка EntityManagerFactory, выбор реализации пула коннектов. Особое внимание уделяется работе с базой данных: через Spring JDBC, Spring ORM и Spring Data Jpa. -- Используются самые востребованные на сегодняшний момент фреймворки: Maven, Spring Security 4 вместе с Spring Security Test, наиболее удобный для работы с базой проекта Spring Data Jpa, библиотека логирования Logback, реализующая SLF4J, повсеместно используемый Bootstrap и jQuery. - -#### Демо разрабатываемого приложения - -## План проекта (ссылки на некоторые темы открыты для просмотра) -### Архитектура проекта. Персистентность. -- Системы управления версиями -- Java 8: Lambda, Stream API -- Обзор используемых в проекте технологий и инструментов. -- Инструмент сборки Maven -- WAR. Веб-контейнер Tomcat. Сервлеты. -- Логирование. -- Обзор стандартных библиотек. Apache Commons, Guava -- Слои приложения. Создание каркаса приложения. -- Обзор Spring Framework. Spring Context. -- Тестирование через JUnit. -- Spring Test -- Базы данных. PostgreSQL. Обзор NoSQL и Java persistence solution без ORM. -- Настройка Database в IDEA. -- Скрипты инициализации базы. Spring Jdbc Template. -- Spring: инициализация и популирование БД -- ORM. Hibernate. JPA. -- [Тестирование JPA-сервиса через AssertJ](https://www.youtube.com/watch?v=BlyaXT6tOaw) -- Поддержка HSQLDB -- Транзакции -- Профили Maven и Spring -- Пул коннектов -- Spring Data JPA -- Кэш Hibernate - -### Разработка WEB -- Spring кэш -- Spring Web -- JSP, JSTL, i18n -- Tomcat maven plugin. JNDI -- Spring Web MVC -- Spring Internationalization -- Тестирование Spring MVC -- REST-контроллеры -- Тестирование REST-контроллеров. Jackson. -- jackson-datatype-hibernate. Тестирование через матчеры. -- Тестирование через SoapUi. UTF-8 -- WebJars. -- Bootstrap. jQuery datatables. -- AJAX. jQuery. Notifications. -- Spring Security -- Spring Binding/Validation -- Работа с DataTables через Ajax. -- Spring Security Test -- [Кастомизация JSON (@JsonView) и валидации (groups)](https://drive.google.com/file/d/0B9Ye2auQ_NsFRTFsTjVHR2dXczA/view?usp=sharing&resourcekey=0-Ou4A_gRor5HaRho4Fciqdw) -- Encoding password -- CSRF (добавление в проект защиты от межсайтовой подделки запроса) -- form-login. Spring Security Taglib -- Handler interceptor -- Spring Exception Handling -- Смена локали -- Фильтрация JSON с помощью @JsonView -- Защита от XSS (Cross Site Scripting) -- Деплой на [собственный выделенный сервер](https://github.com/JavaOPs/startup) -- Локализация datatables, ошибок валидации -- Обработка ошибок 404 (NotFound) -- Доступ к AuthorizedUser -- Собеседование. Разработка ПО - -### Миграция на Spring Boot -- Основы Spring Boot. Spring Boot maven plugin -- Lombok, база H2, ApplicationRunner -- Spring Data REST + HATEOAS -- Swagger/ OpenAPI 3.0 -- Тестирование и кэширование в Spring Boot -- Миграция приложения TopJava на Spring Boot diff --git a/doc/lesson07.md b/doc/lesson07.md deleted file mode 100644 index b958f9d2a74a..000000000000 --- a/doc/lesson07.md +++ /dev/null @@ -1,566 +0,0 @@ -# [Онлайн стажировка Spring 5/JPA Enterprise (TopJava)](http://javaops.ru/view/topjava) -## [Почему мы?](http://javaops.ru/#why) - -## REST, REST-контроллеры, тестирование контроллеров Spring MVC -# Для просмотра открыты видео [4](#--4-миграция-на-junit-5), [5](#-5-принципы-rest-rest-контроллеры), [6](#-6-тестирование-rest-контроллеров-jackson), [7](#-7-кастомизация-jackson-object-mapper), [8](#user-content--8-тестирование-rest-контроллеров-через-jsonassert-и-матчеры) -- Не стоит стремиться прочитать все ссылки урока, их можно использовать как справочник. Гораздо важнее пройти основной материал урока и сделать домашнее задание -- Обязательно посмотри правила работы с патчами на проекте -- Делать Apply Patch лучше по одному непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (`Version Control -> Local Changes -> Ctrl+D`) -- При первом Apply удобнее выбрать имя локального ченджлиста Name: Default. Далее все остальные патчи также будут в него попадать. -- Код проекта обновляется и не всегда совпадает с видео (можно увидеть, как развивался проект). Изменения в проекте указываю после соответствующего патча. - -## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW6 - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW6 -#### Apply 7_01_HW6_fix_tests.patch - -#### Apply 7_02_HW6_meals.patch - -> сделал фильтрацию еды через `get`: операция идемпотентная, можно делать в браузере обновление по F5 - -### Внимание: чиним пути в следующем патче - -#### Apply 7_03_HW6_fix_relative_url_utf8.patch - -- - Relative paths in JSP -- - Spring redirect: prefix - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW6 Optional - -#### Apply 7_04_HW6_optional_add_role.patch - -#### `JdbcUserServiceTest` отвалились. Будем чинить в `7_06_HW6_jdbc_transaction_roles.patch` - -#### Apply 7_05_fix_hint_graph.patch - -- В `JpaUserRepositoryImpl.getByEmail` DISTINCT попадает в запрос, хотя он там не нужен. Это просто указание Hibernate - не дублировать данные. Для оптимизации можно указать Hibernate делать запрос без - distinct: [15.16.2. Using DISTINCT with entity queries](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct) -- Бага [HINT_PASS_DISTINCT_THROUGH does not work if 'hibernate.use_sql_comments=true'](https://hibernate.atlassian.net/browse/HHH-13280). При `hibernate.use_sql_comments=false` все работает - в SELECT нет DISTINCT. -- Тест `DataJpaUserServiceTest.getWithMeals()` не работает для admin (у админа 2 роли, и еда при JOIN дублируется). ... - -#### Apply 7_06_HW6_jdbc_transaction_roles.patch - -Еще интересные JDBC реализации: ... - -### Валидация для `JdbcUserRepository` через Bean Validation API - -#### Apply 7_07_HW6_optional_jdbc_validation.patch - -- [Валидация данных при помощи Bean Validation API](https://alexkosarev.name/2018/07/30/bean-validation-api/). - -На данный момент у нас реализована валидация сущностей только для jpa- и dataJpa-репозиториев. При работе -через JDBC-репозиторий может произойти попытка записи в БД некорректных данных, что приведет к `SQLException` из-за нарушения -ограничений, наложенных на столбцы базы данных. Для того чтобы перехватить невалидные данные еще до -обращения в базу, воспользуемся API *javax.validation* (ее реализация `hibernate-validator` используется для проверки данных в Hibernate и будет использоваться в Spring Validation, которую подключим позже). -В `ValidationUtil` создадим один потокобезопасный валидатор, который можно переиспользовать (см. *javadoc*). -С его помощью в методах сохранения и обновления сущности в jdbc-репозиториях мы можем производить валидацию этой сущности: `ValidationUtil.validate(object);` -Чтобы проверка не падала, `@NotNull Meal.user` пришлось пока закомментировать. Починим в 10-м занятии через `@JsonView`. - -### Отключение кэша в тестах: - -Вместо наших приседаний с `JpaUtil` и проверкой профилей мы можем ... - -#### Apply 7_08_HW06_optional2_disable_tests_cache.patch - -- [Example of PropertyOverrideConfigurer](https://www.concretepage.com/spring/example_propertyoverrideconfigurer_spring) -- [Spring util schema](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#xsd-schemas-util) - -## Занятие 7: - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Тестирование Spring MVC - -
- Краткое содержание - -#### Тестирование Spring MVC - -Для более удобного сравнения объектов в тестах мы будем использовать библиотеку *Harmcrest* с Matcher'ами, которая -позволяет делать сложные проверки. С *JUnit* по умолчанию подтягивается *Harmcrest core*, но нам потребуется расширенная версия: -в `pom.xml` из зависимости JUnit исключим дочернюю `hamcrest-core` и добавим `hamcrest-all`. - -Для тестирования web создадим вспомогательный класс `AbstractControllerTest`, от которого будут наследоваться все -тесты контроллеров. Его особенностью будет наличие `MockMvc` - эмуляции Spring MVC для тестирования web-компонентов. -Инициализируем ее в методе, отмеченном `@PostConstruct`: - - ``` -mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilter(CHARACTER_ENCODING_FILTER).build(); - ``` - -Для того чтобы в тестах контроллеров не популировать базу перед каждым тестом, пометим этот базовый тестовый класс аннотацией `@Transactional`. -Теперь каждый тестовый метод будет выполняться в транзакции, которая будет откатываться после окончания метода и возвращать базу данных в исходное -состояние. Однако теперь в работе тестов могут возникнуть нюансы, связанные с пропагацией транзакций: все -транзакции репозиториев станут вложенными во внешнюю транзакцию теста. При этом, например, кэш первого уровня станет работать не -так, как ожидается. Т. е. при таком подходе нужно быть готовыми к ошибкам: мы их увидим и поборем в тестах на обработку ошибок на последних занятиях TopJava. - -#### UserControllerTest - -Создадим тестовый класс для контроллера юзеров, он должен наследоваться от `AbstractControllerTest`. -В `MockMvc` используется [паттерн проектирования Builder](https://refactoring.guru/ru/design-patterns/builder). - - ``` - mockMvc.perform(get("/users")) // выполнить HTTP метод GET к "/users" - .andDo(print()) // распечатать содержимое ответа - .andExpect(status().isOk()) // от контроллера ожидается ответ со статусом HTTP 200(ok) - .andExpect(view().name("users")) // контроллер должен вернуть view с именем "users" - .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) // ожидается, что клиент должен быть перенаправлен на "/WEB-INF/jsp/users.jsp" - .andExpect(model().attribute("users", hasSize(2))) // в модели должен быть атрибут "users" размером = 2 ... - .andExpect(model().attribute("users", hasItem( // ... внутри которого есть элемент ... - allOf( - hasProperty("id", is(START_SEQ)), // ... с аттрибутом id = START_SEQ - hasProperty("name", is(USER.getName())) //... и name = user - ) - ))); -} - ``` - -В параметры метода `andExpect()` передается реализация `ResultMatcher`, в которой мы определяем, как должен быть обработан ответ контроллера. - -
- -#### Apply 7_09_controller_test.patch - -> - в `MockMvc` добавился `CharacterEncodingFilter` -> - добавил [`AllActiveProfileResolver`](//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver) для возвращения массива профилей -> - сделал вспомогательный метод `AbstractControllerTest.perform()` - -- Hamcrest -- Unit Testing of Spring MVC Controllers - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. [Миграция на JUnit 5](https://drive.google.com/open?id=16wi0AJLelso-dPuDj6xaGL7yJPmiO71e) - -
- Краткое содержание - -Для миграции на 5-ю версию JUnit в файле `pom.xml` поменяем зависимость `junit` на `junit-jupiter-engine` ([No need `junit-platform-surefire-provider` dependency in `maven-surefire-plugin`](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven)). -Актуальную версию всегда можно посмотреть [в центральном maven-репозитории](https://search.maven.org/search?q=junit-jupiter-engine), берем только релизы (..-Mx означают предварительные milestone версии) -Изменять конфигурацию плагина `maven-sureface-plugin` в новых версиях JUnit уже не требуется. -JUnit5 не содержит в себе зависимости от *Harmcrest* (которую нам приходилось вручную -отключать для JUnit4 в предыдущих шагах), поэтому исключение `hamcrest-core` просто удаляем. -В итоге у нас останутся зависимости JUnit5 и расширенный Harmcrest. -Теперь мы можем применить все нововведения пятой версии в наших тестах: - 1. Для всех тестов теперь мы можем удалить `public`. - 2. Аннотацию `@Before` исправим на `@BeforeEach` - теперь метод, который будет выполняться перед -каждым тестом, помечается именно так. - 3. В JUnit5 работа с исключениями похожа на JUnit4 версии 4.13: вместо ожидаемых исключений в параметрах аннотации `@Test(expected = Exception.class)` используется метод `assertThrows()`, -в который первым аргументом мы передаем ожидаемое исключение, а вторым аргументом — реализацию функционального интерфейса `Executable` (код, -в котором ожидается возникновение исключения). - 4. Метод `assertThrows()` возвращает исключение, которое было выброшено в переданном ему коде. Теперь мы можем получить это исключение, извлечь из него сообщение с помощью - `e.getMessage()` и сравнить с ожидаемым. - 5. Для теста на валидацию при проверке предусловия, только при выполнении которого -будет выполняться следующий участок кода (например, в нашем случае тесты на валидацию выполнялись -только в jpa профиле), теперь нужно пользоваться утильным методом `Assumptions` (нам уже не требуется). - 6. Проверку Root Cause - причины, из-за которой было выброшено пойманное исключение, мы будем делать позднее, при тестах на ошибки. - 7. Из JUnit5 исключена функциональность `@Rule`, вместо них теперь нужно использовать `Extensions`, которые -могут встраиваться в любую фазу тестов. Чтобы добавить их в тесты, пометим базовый тестовый класс аннотацией `@ExtendWith`. - -JUnit предоставляет нам набор коллбэков — интерфейсов, которые будут исполняться в определенный момент тестирования. -Создадим класс `TimingExtension`, который будет засекать время выполнения тестовых методов. -Этот класс будет имплементировать маркерные интерфейсы — коллбэки JUnit: - - `BeforeTestExecutionCallback` - коллбэк, который будет вызывать методы этого интерфейса перед каждым тестовым методом. - - `AfterTestExecutionCallback` - методы этого интерфейса будут вызываться после каждого тестового метода; - - `BeforeAllCallback` - методы перед выполнением тестового класса; - - `AfterAllCallback` - методы после выполнения тестового класса; - -Осталось реализовать соответствующие методы, которые описываются в каждом из этих интерфейсов, они и будут вызываться JUnit в нужный момент: - - в методе `beforeAll` (который будет вызван перед запуском тестового класса) создадим спринговый утильный секундомер `StopWatch` для текущего тестового класса; - - в методе `beforeTestExecution` (будет вызван перед тестовым методом) - запустим секундомер; - - в методе `afterTestExecution` (будет вызван после тестового метода) - остановим секундомер. - - в методе `afterAll` (который будет вызван по окончанию работы тестового класса) - выведем результат работы этого секундомера в лог; - -8. Аннотации `@ContextConfiguration` и `@ExtendWith(SpringExtension.class)` (замена `@RunWith`) мы можем заменить одной `@SpringJUnitConfiguration` (старые версии IDEA ее не понимают) - -
- -#### Apply 7_10_JUnit5.patch - -> - [No need `junit-platform-surefire-provider` dependency in `maven-surefire-plugin`](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven) -> - [Наконец пофиксили баг с `@SpringJUnitConfig`](https://youtrack.jetbrains.com/issue/IDEA-166549) - -- [JUnit 5 homepage](https://junit.org/junit5) -- [Overview](https://junit.org/junit5/docs/snapshot/user-guide/#overview) -- [10 интересных нововведений](https://habr.com/post/337700) -- Дополнительно: - - [Extension Model](https://junit.org/junit5/docs/current/user-guide/#extensions) - - [A Guide to JUnit 5](http://www.baeldung.com/junit-5) - - [Migrating from JUnit 4](http://www.baeldung.com/junit-5-migration) - - [Before and After Test Execution Callbacks](https://junit.org/junit5/docs/snapshot/user-guide/#extensions-lifecycle-callbacks-before-after-execution) - - [Conditional Test Execution](https://junit.org/junit5/docs/snapshot/user-guide/#writing-tests-conditional-execution) - - [Third party Extensions](https://github.com/junit-team/junit5/wiki/Third-party-Extensions) - - [Реализация assertThat](https://stackoverflow.com/questions/43280250) - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. [Принципы REST. REST контроллеры](https://drive.google.com/open?id=1e4ySjV15ZbswqzL29UkRSdGb4lcxXFm1) - -
- Краткое содержание - -#### Принципы REST, REST-контроллеры - -> [REST](http://spring-projects.ru/understanding/rest/) - архитектурный стиль проектирования распределенных систем (типа клиент-сервер). - -Чаще всего в REST-сервер и клиент общаются посредством обмена JSON-объектами через HTTP-методы GET/POST/PUT/DELETE/PATCH. -Особенностью REST является отсутствие состояния (контекста) взаимодействий клиента и сервера. - -В нашем приложении есть контроллеры для Admin и для User. Чтобы сделать их REST-контроллерами, -заменим аннотацию `@Controller` на `@RestController` - -> Не поленитесь зайти через Ctrl+click в `@RestController`: к аннотации `@Controller` добавлена `@ResponseBody`. Т. е. ответ от нашего приложения будет не View, а данные в теле ответа. - -В `@RequestMapping`, кроме пути для методов контроллера (`value`), добавляем параметр `produces = MediaType.APPLICATION_JSON_VALUE`. -Это означает, что в заголовки ответа будет добавлен тип `ContentType="application/json"` - в ответе от контроллера будет приходить JSON-объект. - -> Чтобы было удобно использовать путь к этому контроллеру в приложении и в тестах, -> выделим путь к нему в константу REST_URL, к которой можно будет обращаться из других классов - -1. Метод `AdminRestController.getAll` пометим аннотацией `@GetMapping` - маршрутизация к методу по HTTP GET. - -2. Метод `AdminRestController.get` пометим аннотацией `@GetMapping("/{id}")`. -В скобках аннотации указано, что к основному URL контроллера будет добавляться `id` пользователя - переменная, которая передается в запросе непосредственно в URL. - Соответствующий параметр метода нужно пометить аннотацией `@PathVariable` (если имя в URL и имя аргумента метода не совпадают, в параметрах аннотации дополнительно нужно будет уточнить - имя в URL. Если они совпадают, [этого не требуется](https://habr.com/ru/post/440214/). - -3. Метод создания пользователя `create` отметим аннотацией `@PostMapping` - маршрутизация к методу по HTTP POST. - В метод мы передаем объект `User` в теле запроса (аннотация `@RequestBody`) в формате JSON (`consumes = MediaType.APPLICATION_JSON_VALUE`). - При создании нового ресурса правило хорошего тона - вернуть в заголовке ответа URL созданного ресурса. - Для этого возвращаем не `User`, а `ResponseEntity`, который мы можем с помощью билдера `ServletUriComponentsBuilder` дополнить заголовком ответа `Location` и вернуть статус `CREATED(201)` - (если пойти в код `ResponseEntity.created` можно докопаться до сути, очень рекомендую смотреть в исходники кода). - -4. Метод `delete` помечаем `@DeleteMapping("/{id}")` - HTTP DELETE. - Он ничего не возвращает, поэтому помечаем его аннотацией `@ResponseStatus(HttpStatus.NO_CONTENT)`. Статус ответа будет HTTP.204; - -5. Над методом обновления ставим `@PutMapping` (HTTP PUT). В аргументах метод принимает `@RequestBody User user` и `@PathVariable int id`. - -6. Метод поиска по `email` также помечаем `@GetMapping` и, чтобы не было конфликта маршрутизации с методом `get()`, - указываем в URL добавку `/by`. В этот метод `email` передается как параметр запроса (аннотация `@RequestParam`). - -> **Все это СТАНДАРТ архитектурного стиля REST. НЕ придумывайте ничего своего в своих выпускных проектах! Это очень большая ошибка - не придерживаться стандартов API.** - -7. `ProfileRestController` выполняем аналогичным способом с учетом того, что пользователь имеет доступ только к своим данным. - -Если на данном этапе попытаться запустить приложение и обратиться к какому-либо методу контроллера, сервер ответит нам ошибкой со статусом 406, -так как Spring не знает, как преобразовать объект User в JSON... - -
- -#### Apply 7_11_rest_controller.patch - -- Понимание REST -- JSON (JavaScript Object Notation) -- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) -- [10 Best Practices for Better RESTful](https://medium.com/@mwaysolutions/10-best-practices-for-better-restful-api-cbe81b06f291) -- [Best practices for rest nested resources](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources) -- - Request mapping -- [Лучшие практики разработки REST API: правила 1-7,15-17](https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/) -- Дополнительно: - - [Подборка практик REST](https://gist.github.com/Londeren/838c8a223b92aa4017d3734d663a0ba3) - - JAX-RS vs Spring MVC - - RESTful API для сервера – делаем правильно (Часть 1) - - RESTful API для сервера – делаем правильно (Часть 2) - - И. Головач. - RestAPI - - [value/name в аннотациях @PathVariable и @RequestParam](https://habr.com/ru/post/440214/) - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. [Тестирование REST контроллеров. Jackson.](https://drive.google.com/open?id=1aZm2qoMh4yL_-i3HhRoyZFjRAQx-15lO) - -
- Краткое содержание - -Для работы с JSON добавляем в `pom.xml` зависимость `jackson-databind`. -Актуальную версию библиотеки можно посмотреть в [центральном maven-репозитории](https://search.maven.org/artifact/com.fasterxml.jackson.core/jackson-databind). -Теперь Spring будет автоматически использовать эту библиотеку для сериализации/десериализации объектов в JSON (найдя ее в *classpath*). -Если сейчас запустить приложение и обратиться к методам REST-контроллера, то оно выбросит `LazyInitializationException`. -Оно возникает из-за того, что у наших сущностей есть лениво загружаемые поля, отмеченные `FetchType.LAZY` - при загрузке сущности из базы вместо такого поля подставится Proxy, который и должен вернуть -реальный экземпляр этого поля при первом же обращении. Jackson при сериализации в JSON использует все поля сущности, -и при обращении к *Lazy*-полям возникает исключение, так как сессия работы с БД в этот момент уже закрыта, и нужный объект -не может быть инициализирован. Чтобы Jackson игнорировал эти поля, пометим их аннотацией `@JsonIgnore`. - -Теперь при запуске приложения REST-контроллер будет работать. Но при получении JSON-объектов мы можем увидеть, что Jackson сериализовал объект -через геттеры (например, в ответе есть поле `new` от метода `Persistable.isNew()`). -Чтобы учитывались только поля объектов, добавим над `AbstractBaseEntity`: -````java -@JsonAutoDetect(fieldVisibility = ANY, // jackson видит все поля - getterVisibility = NONE, // ... но не видит геттеров - isGetterVisibility = NONE, //... не видит геттеров boolean-полей - setterVisibility = NONE) // ... не видит сеттеров -```` -Теперь все сущности, унаследованные от базового класса, будут сериализоваться/десериализоваться через поля. - -
- -#### Apply 7_12_rest_test_jackson.patch - -- [Jackson databind github](https://github.com/FasterXML/jackson-databind) -- [Jackson Annotation Examples](https://www.baeldung.com/jackson-annotations) - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. [Кастомизация Jackson Object Mapper](https://drive.google.com/open?id=1CM6y1JhKG_yeLQE_iCDONnI7Agi4pBks) - -
- Краткое содержание - -Сейчас, чтобы не сериализовать *Lazy*-поля, мы должны пройтись по каждой сущности и -вручную пометить их аннотацией `@JsonIgnore`. Это неудобно, засоряет код и допускает возможные ошибки. К тому же, -при некоторых условиях, нам иногда нужно загрузить и в ответе передать эти *Lazy*-поля. -Чтобы запретить сериализацию Lazy-полей для всего проекта, подключим в `pom.xml` библиотеку `jackson-datatype-hibernate`. -Также изменим сериализацию/десериализацию полей объектов в JSON: не через аннотацию `@JsonAutoDetect`, а в классе `JacksonObjectMapper`, который -унаследуем от `ObjectMapper` (стандартный Mapper, который использует Jackson) и сделаем в нем другие настройки. -В конструкторе: -- регистрируем `Hibernate5Module` - модуль `jackson-datatype-hibernate`, который не делает сериализацию ленивых полей. -- модуль для корректной сериализации `LocalDateTime` в поля JSON - `JavaTimeModule` модуль библиотеки `jackson-datatype-jsr310` -- запрещаем доступ ко всем полям и методам класса и потом разрешаем доступ только к полям -- не сериализуем null-поля (`setSerializationInclusion(JsonInclude.Include.NON_NULL)`) - -Чтобы подключить наш кастомный `JacksonObjectMapper` в проект, в конфигурации `spring-mvc.xml` к -настройке `` добавим `MappingJackson2HttpMessageConverter`, который будет использовать наш маппер. - -
- - -#### Apply 7_13_jackson_object_mapper.patch - -- Сериализация hibernate lazy-loading с помощью - jackson-datatype-hibernate -- Handle Java 8 dates with Jackson -- Дополнительно: - - Jackson JSON - Serializer & Deserializer - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. [Тестирование REST-контроллеров через JSONassert и Матчеры](https://drive.google.com/open?id=1oa3e0_tG57E71g6PW7_tfb3B61Qldctl) - -
- Краткое содержание - -Сейчас в тестах REST-контроллера мы проводим проверку только на статус ответа и тип возвращаемого контента. Добавим проверку содержимого ответа. - -#### 7_14_json_assert_tests - -Чтобы сравнивать содержимое ответа контроллера в виде JSON и сущность, воспользуемся библиотекой -`jsonassert`, которую подключим в `pom.xml` со scope *test*. - -Эта библиотека при сравнении в тестах в качестве ожидаемого значения ожидает от -нас объект в виде JSON-строки. Чтобы вручную не преобразовывать объекты в JSON и не -хардкодить их в виде строк в наши тесты, воспользуемся Jackson. -Для преобразования объектов в JSON и обратно создадим утильный класс `JsonUtil`, в котором -с помощью нашего `JacksonObjectMapper` и будет конвертировать объекты. -И мы сталкиваемся с проблемой: `JsonUtil` - утильный класс и не является -бином Spring, а для его работы требуется наш кастомный маппер, который находится под управлением -Spring и расположен в контейнере зависимостей. Поэтому, чтобы была возможность получить -наш маппер из других классов, сделаем его синглтоном и сделаем в нем статический -метод, который будет возвращать его экземпляр. Теперь `JsonUtil` сможет его получить. -И нам нужно указать Spring, чтобы он не создавал второй экземпляр этого объекта, а клал в свой контекст существующий. -Для этого в конфигурации `spring-mvc.xml` определим factory-метод, с помощью которого Spring должен -получить экземпляр (instance) этого класса: -```xml - -``` -а в конфигурации `message-converter` вместо создания бина просто сошлемся на сконфигурированный `objectMapper`. - -Метод `ContentResultMatchers.json()` из `spring-test` использует библиотеку `jsonassert` для сравнения 2-х JSON строк: одну из ответа контроллера и вторую - -JSON-сериализация `admin` без поля `registered` (это поле инициализируется в момент создания и отличается). -В методе `JsonUtil.writeIgnoreProps` мы преобразуем объект `admin` в мапу, удаляем из нее игнорируемые поля и снова сериализуем в JSON. - -Также сделаем тесты для утильного класса `JsonUtil`. В тестах мы записываем -объект в JSON-строку, затем конвертируем эту строку обратно в объект и сравниваем с исходным. И то же самое делаем со списком объектов. - -#### 7_15_tests_refactoring - -**`RootControllerTest`** - -Сделаем рефакторинг `RootControllerTest`. Ранее мы в тесте получали модель, доставали из нее сущности и с помощью `hamcrest-all` -производили по одному параметру их сравнение с ожидаемыми значениями. -Метод `ResultActions.andExpect()` позволяет передавать реализацию интерфейса `Matcher`, в котором можно делать любые сравнения. -Функциональность сравнения списка юзеров по ВСЕМ полям у нас уже есть - мы просто делегируем сравнение объектов в `UserTestData.MATCHER`. -При этом нам больше не нужен `harmcrest-all`, нам достаточно только `harmcrest-core`. - -**`MatcherFactory`** - -Теперь вместо `jsonassert` и сравнения JSON-строк в тестах контроллеров сделаем сравнения JSON-объектов через `MatcherFactory`. -Преобразуем ответ контроллера из JSON в объект и сравним с эталоном через уже имеющийся у нас матчер. -Вместо сравнения JSON-строк в метод `andExpect()` мы будем передавать реализации интерфейса `ResultMatcher` из `MATCHER.contentJson(..)`. - -`MATCHER.contentJson(..)` принимают ожидаемый объект и возвращают для него `ResultMatcher` с реализацией единственного метода `match(MvcResult result)`, -в котором делегируем сравнение уже существующим у нас матчерам. -Мы берем JSON-тело ответа (`MatcherFactory.getContent`), десериализуем его в объект (`JsonUtil.readValue/readValues`) и сравниваем через имеющийся `MATCHER.assertMatch` -десериализованный из тела контроллера объект и ожидаемое значение. - -> Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил. - -**`AdminRestControllerTest`** - -- `getByEmail()` - сделан по аналогии с тестом `get()`. Дополнительно нужно добавить в строку URL параметры запроса. -- `delete()` - выполняем HTTP.DELETE. Проверяем статус ответа 204. Проверяем, что пользователь удален. - -> Раньше я получал всех users из базы и проверял, что среди них нет удаленного. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении/удалении новых тестовых данных. - -- `update()` - выполняем HTTP.PUT. В тело запроса подаем сериализованный `JsonUtil.writeValue(updated)`. После выполнения проверяем, что объект в базе обновился. -- `create()` - выполняем HTTP.POST аналогично `update()`. Но сравнить результат мы сразу не можем, т. к. при создании объекта ему присваивается `id`. - Поэтому мы извлекаем созданного пользователя из ответа (`MATCHER.readFromJson(action)`), получаем его `id`, и уже с этим `id` эталонный объект мы можем сравнить с объектом в ответе контроллера и со - значением в базе. -- `getAll()` - аналогично `get()`. Список пользователей из ответа в формате JSON сравниваем с эталонным списком (`MATCHER.contentJson(admin, user)`). - -Тесты для `ProfileRestController` выполнены аналогично. - -
- -#### Apply 7_14_json_assert_tests.patch - -> - В `JsonUtil.writeIgnoreProps` вместо цикла по мапе сделал `map.keySet().removeAll` - -- [JSONassert](https://github.com/skyscreamer/JSONassert) -- [Java Code Examples for ObjectMapper](https://www.programcreek.com/java-api-examples/index.php?api=com.fasterxml.jackson.databind.ObjectMapper) - -#### Apply 7_15_tests_refactoring.patch - -> - Методы из класса `TestUtil` перенес в `MatcherFactory`, лишние удалил. -> - Раньше в тестах я для проверок получал всех users из базы и сравнивал с эталонным списком. При этом тесты становятся чувствительными ко всем users в базе и ломаются при добавлении/удалении новых тестовых данных. - -- [Java @SafeVarargs Annotation](https://www.baeldung.com/java-safevarargs) - -### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Тестирование через SoapUI. UTF-8 - -
- Краткое содержание - -SoapUI - это один из инструментов для тестирования API приложений, которые работают по REST и по SOAP. -Он позволяет нам по протоколу HTTP дернуть методы нашего API и увидеть ответ контроллеров. - -Если в контроллер мы добавим метод, который в теле ответа будет возвращать текст на кириллице, то увидим, что кодировка теряется. -Для сохранения кодировки используем `StringHttpMessageConverter`, который конфигурируем в `spring-mvc.xml`. -При этом мы должны явно указать, что конвертор будет работать только с текстом в кодировке *UTF-8*. - -
- -#### Apply 7_16_soapui_utf8_converter.patch - -- Инструменты тестирования REST: - - SoapUI - - Написание HTTP-запросов с помощью - Curl. - Для Windows 7 можно использовать Git Bash, с Windows 10 v1803 можно прямо из консоли. Возможны проблемы с UTF-8: - - [CURL doesn't encode UTF-8](https://stackoverflow.com/a/41384903/548473) - - [Нстройка кодировки в Windows](https://support.socialkit.ru/ru/knowledge-bases/4/articles/11110-preduprezhdenie-obnaruzhenyi-problemyi-svyazannyie-s-raspoznavaniem-russkih-simvolov) - - **[IDEA: Tools->HTTP Client->...](https://www.jetbrains.com/help/idea/rest-client-tool-window.html)** - - Postman - - [Insomnia REST client](https://insomnia.rest/) - -**Импортировать проект в SoapUI из `config\Topjava-soapui-project.xml`. Response смотреть в формате JSON.** - -> Проверка UTF-8: http://localhost:8080/topjava/rest/profile/text - -[ResponseBody and UTF-8](http://web.archive.org/web/20190102203042/http://forum.spring.io/forum/spring-projects/web/74209-responsebody-and-utf-8) - -## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы - -> Зачем у нас и UI-контроллеры, и REST-контроллеры? То есть в общем случае backend-разработчику недостаточно предоставить REST API и RestController? - -В общем случае нужны и те, и другие. REST обычно используют для отдельного UI (например, на React или Angular) или для -интеграции / мобильного приложения. У нас REST-контроллеры используются только для тестирования. UI-контроллеры используем для -нашего приложения на JSP шаблонах. Таких сайтов без богатой UI логики тоже немало. Например https://javaops.ru/ :) -Разница в запросах: - -- для UI используются только GET и POST -- при создании/обновлении в UI мы принимаем данные из формы `application/x-www-form-urlencoded` (посмотрите - вкладку `Network`, не в формате JSON) -- для REST API запросы GET, POST, PUT, DELETE, PATCH и возвращают только данные (обычно JSON) - -...и в способе авторизации: - -- для REST API у нас будет базовая авторизация -- для UI - через cookies - -Также часто бывают смешанные сайты, где есть и отдельное JS приложение, и шаблоны. - -> При выполнении тестов через MockMvc никаких изменений на базе не видно, почему оно не сохраняет? - -`AbstractControllerTest` аннотируется `@Transactional` - это означает, что тесты идут в транзакции, и после каждого -теста JUnit делает rollback базы. - -> Что получается в результате выполнения запроса `SELECT DISTINCT(u) FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email`? В чем разница в SQL без `DISTINCT`. - -Запросы SQL можно посмотреть в логах. Т. е. `DISTINCT` в `JPQL` влияет на то, как Hibernate обрабатывает дублирующиеся -записи (с `DISTINCT` их исключает). Результат можно посмотреть в тестах или приложении, поставив брекпойнт. По -поводу `SQL DISTINCT` не стесняйтесь пользоваться google, -например, [оператор SQL DISTINCT](http://2sql.ru/novosti/sql-distinct/) - -> В чем заключается расширение функциональности hamcrest в нашем тесте, что нам пришлось его отдельно от JUnit прописывать? - -hamcrest-all используется в проверках `RootControllerTest`: `org.hamcrest.Matchers.*` - -> Jackson мы просто подключаем в помнике, и Spring будет с ним работать без любых других настроек? - -Да, Spring смотрит в classpath и если видит там Jackson, то подключает интеграцию с ним. - -> Где-то слышал, что любой ресурс по REST должен однозначно идентифицироваться через url без параметров. Правильно ли задавать URL для фильтрации в виде `http://localhost/topjava/rest/meals/filter/{startDate}/{startTime}/{endDate}/{endTime}` ? - -Так делают только при -отношении -агрегация, например, если давать админу право смотреть еду любого юзера, URL мог бы быть похож -на `http://localhost/topjava/rest/users/{userId}/meals/{mealId}` (не рекомендуется, см. ссылку ниже). В случае критериев -поиска или страничных данных они передаются как параметр. Смотри также: - -- [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) -- 10 Best Practices - for Better RESTful -- [REST resource hierarchy (если кратко: не рекомендуется)](https://stackoverflow.com/questions/15259843/how-to-structure-rest-resource-hierarchy) - -> Что означает конструкция в `JsonUtil`: `reader.readValues(json)`; - -См. Generic Methods. Когда компилятор -не может вывести тип, можно его уточнить при вызове generic метода. Неважно, static или нет. - -## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW07 - -- 1: Добавить тесты контроллеров: - - 1.1 `RootControllerTest.getMeals` для `meals.jsp` - - 1.2 Сделать `ResourceControllerTest` для `style.css` (проверить `status` и `ContentType`) -- 2: Реализовать `MealRestController` и протестировать его через `MealRestControllerTest` - - 2.1 следите, чтобы url в тестах совпадал с параметрами в методе контроллера. Можно добавить - логирование `` для проверки маршрутизации. - - 2.2 в параметрах `getBetween` принимать `LocalDateTime` (конвертировать - через @DateTimeFormat with Java - 8 Date-Time API), пока без проверки на `null` (используя `toLocalDate()/toLocalTime()`, см. Optional п. 3). В - тестах передавать в формате `ISO_LOCAL_DATE_TIME` ( - например `'2011-12-03T10:15:30'`). - -### Optional - -- 3: Переделать `MealRestController.getBetween` на параметры `LocalDate/LocalTime` c раздельной фильтрацией по - времени/дате, работающий при `null` значениях (см. демо и `JspMealController.getBetween`) - . Заменить `@DateTimeFormat` на свои LocalDate/LocalTime конверторы или форматтеры. - - Spring Type - Conversion - - Spring Field - Formatting - - - Difference between Spring MVC formatters and converters -- 4: Протестировать `MealRestController` (SoapUI, Curl, IDEA Test RESTful Web Service, Postman). Запросы `curl` занести - в отдельный `md` файл (или `README.md`) -- 5: Добавить в `AdminRestController` и `ProfileRestController` методы получения пользователя вместе с - едой (`getWithMeals`, `/with-meals`). - - [Jackson – Bidirectional Relationships](https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion) - -### Optional 2 - -- 6: Сделать тесты на методы контроллеров `getWithMeals()` (п. 5) - -**На следующем занятии используется JavaScript/jQuery. Если у вас там -пробелы, пройдите его основы** ---------------------- - -## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки и подсказки по реализации - -- 1: Ошибка в тесте _Invalid read array from JSON_ обычно расшифровывается немного ниже: читайте внимательно. -- 2: Jackson и неизменяемые объекты (для - сериализации `MealTo`) -- 3: Если у meal, приходящий в контроллер, поля `null`, проверьте `@RequestBody` перед параметром (данные приходят в - формате JSON) -- 4: При проблемах с собственным форматтером убедитесь, что в конфигурации `[JVM Ecosystem Report 2021](https://snyk.io/jvm-ecosystem-report-2021/) ->показывает, что Spring является абсолютным ->лидером среди фреймворков. - -На первом месте упоминается **Spring Boot**, а на втором - **Spring MVC**. - -Здесь хотелось бы внести некоторую ясность. -"Сердцем" Spring является Spring Core, который реализует Dependency -Injection. -Этот компонент, как правило, используется во всех остальных -проектах Spring Framework. - -Spring Boot - это надстройка над Spring Framework, -которая позволяет быстро создавать приложения разных типов, -использовать автоматическую конфигурацию для некоторых -компонентов и использовать некоторые удобные встроенные -инструменты, например, готовые инструменты для мониторинга -работы приложения (Spring Actuator). - -Если мы создаем web-приложение, то мы можем создавать его с -помощью Spring Boot, но Spring MVC, который является именно -web-фреймворком, также будет присутствовать в таком приложении -и играть ключевую роль. - -Spring Boot позволяет создавать очень простые приложения без -единой строчки конфигурации. Но как только мы пытаемся -создать что-то более сложное, нам все равно приходится -создавать конфигурационные файлы и классы и настраивать все вручную -точно так же, как в традиционном Spring-приложении. -По своему опыту могу сказать, что на реальных проектах -такая ситуация возникает в большинстве случаев. -Например, как только нужно настроить подключение -к нескольким базам данных, а не к одной, мы вынуждены -создавать конфигурационные классы вручную. - -В этом случае Spring Boot в какой-то степени может даже -навредить вашей продуктивности, потому что вам придется -разбираться, какие автоконфигурационные классы отключить, и т. д. - -Мы изучаем Spring довольно глубоко. -Традиционно хорошо показал себя подход, при котором -вначале изучается чистый Spring Framework, после чего -мы переходим к использованию Spring Boot. Такой подход -сохраняется и в этой версии курса. Без этого Spring Boot -будет для вас "черным ящиком", который вам будет очень -сложно понять, настраивать и отлаживать. - -### ORM frameworks -Отчеты, которые мы использовали, обычно не включают информацию -о самых популярных ORM-фреймворках. - ->Известно, ->что **Hibernate** является достаточно популярным фреймворком ->и наиболее часто упоминается в вакансиях, если сравнивать ->его с другими ORM-фреймворками и инструментами для работы Java-приложения ->с базами данных. - -Вот одна из статей, которая также говорит о -популярности Hibernate: [Top 5 Java ORM tools - 2022](https://www.knowledgefactory.net/2021/09/top-java-orm-tools-20XX.html) - -**ORM** или **Object-relational-mapping** можно перевести как -"объектно-реляционное отображение (преобразование)". -Это техника, которая позволяет создать виртуальную базу -данных с помощью объектно ориентированного языка -программирования и взаимодействовать с ней, в то время -как взаимодействие уже с реальной базой данных выполняется -фреймворком незаметно для нас. -Это облегчает работу, позволяет работать с привычными -и удобными для использования Java-объектами, вызывая -их методы, вместо того, чтобы писать SQL-запросы к базе данных и код для интерпретации полученных данных. - ->Hibernate является реализацией спецификации JPA, ->так же, как и менее популярный EclipseLink. - -Говоря простым языком, JPA (Java Persistence API) - это -набор интерфейсов без реализации для работы с ORM, -который включен в стандарт Java EE. Отдельные провайдеры -могут предоставлять свои реализации этого интерфейса. - -Недостатками JPA и его реализации Hibernate являются удар по производительности -и недостаточная гибкость. Вопрос производительности частично -может быть решен с помощью различных техник оптимизации кода, -написанного с использованием JPA, однако для некоторых -приложений тот оверхед, который несет в себе JPA, является -недопустимым, и вы можете увидеть на некоторых проектах -использование других подходов, от использования -чистого JDBC до использования MyBatis, JOOQ или каких-то -альтернативных решений. - -В реальных приложениях те запросы к базе данных, -которые создают наибольшую нагрузку на систему, -могут быть оптимизированы для улучшения производительности: для -них могут быть применены JDBC, альтернативные -JPA-фреймворки, также запросы могут быть написаны -с помощью нативных SQL-запросов в JPA. -Для остальных запросов, которые не приводят к -большой нагрузке на систему, в том же приложении может -быть применен Hibernate. -Таким образом, в одном приложении могут быть применены одновременно разные технологии. - - -На курсе вы изучите, как работать с БД с помощью нескольких -технологий: JDBC (а точнее, Spring JDBC template), -а также JPA и Spring DataJPA. -Вы сами не только увидите, но и почувствуете -при написании кода в ваших домашних заданиях плюсы и минусы каждого подхода. - -### Обзор пройденных тем -Давайте подведем промежуточные итоги по результатам этого урока. - -В этом уроке мы: - -* познакомились с тем, что такое фреймворки -и для чего они нужны; -* посмотрели статистику, которая определяет -Spring Framework как самый популярный фреймворк на рынке; -* познакомились с ORM - подходом для упрощения работы -с базами данных с использованием объектно-ориентированного подхода; -* узнали про JPA (Java Persistence API) - протокол для работы с -ORM - и про реализации JPA, среди которых Hibernate является самой популярной; -* обсудили недостатки подхода ORM и познакомились с инструментами, -которые могут быть использованы в качестве альтернативы. - -В следующем коротком уроке мы завершим обзор используемых технологий. -Увидимся в следующем видео. diff --git a/doc/video2.3.md b/doc/video2.3.md deleted file mode 100644 index af9506cb4596..000000000000 --- a/doc/video2.3.md +++ /dev/null @@ -1,115 +0,0 @@ -## Обзор наиболее востребованных технологий. Тренды - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1XcTRkArj2guek9OiPuFEq_U1V4Dg0N-j) - -### Тренд на отказ от reflection - -Говоря о трендах, хотелось бы сказать, что в настоящее -время развиваются фреймворки, построенные на отказе -от использования Reflection API. -Dependency injection в Spring, сериализация с помощью -Jackson построены на использовании Reflection, что -сильно бьет по производительности, но было достаточно -удобным решение до недавнего времени. -Фреймворк Micronaut полностью построен на отказе от -Reflection, и это дает существенный прирост -производительности при измерении ряда параметров. -Micronaut использует продвинутый компилятор и -генерацию байткода, что позволяет создать все -бины (управляемые фреймворком объекты) в ходе компиляции. - -Пока доля Micronaut очень мала, но он используется -в продакшене. Я предполагаю, что в какой-то момент -Spring может также внедрить подобный подход. - -### Тренд на развитие реактивного программирования - -Продолжает развиваться реактивное программирование, -которое является в какой-то степени новой парадигмой -в программировании. - -Spring развивает свой реактивный фреймворк Spring WebFlux, -который поддерживает библиотеку Reactor. - - -Ряд современных задач не решается традиционными -методами, такими как блокирующий Input/Output, -HTTP-протокол. Одним из решений данных вопросов -является применение подходов реактивного -программирования (Reactor, RxJava), использование -новых протоколов передачи данных (RSocket). -У нас также готовится курс [ReactJava](https://javaops.ru/#inprogress) как -продолжение TopJava на реактивном стеке. - -### Тренд на микросервисы, тезисы - -ПО становиться большим и сложным. Очень большое приложение -сложно поддерживать одной командой, управлять его жизненным -циклом - разработкой, релизами. - -Если в большое монолитное приложение добавляется новая -функциональность приходиться выполнять повторного тестирование -всей большой системы, в результате срок выхода новых фич увеличивается. - -Микросервисная архитектура позволяет решить эту проблему - она -предполагает разделение большого приложения на отдельные -приложения-модули, которые взаимодействуют друг с другом. -Над одним модулем-сервисом может работать один человек или -небольшая команда. У такого модуля-сервиса будет отдельный -Git-репозиторий, он может быть задеплоен (развернут) независимо -от остальной системы. При таком подходе, команды могут вести работу -над различными сервисами параллельно, параллельно деплоить -их на серверах. При внесении изменений в микросервис “А”, -вероятность вызывать проблемы в микросервисе “B” снижается. -У разных микросервисов могут быть собственные базы данных. -Это также повышает надежность и гибкость. Например, если по каким то причинам после обновления банковского микросервиса, ответственного за выдачу кредитов возникли сбои и база данных оказалась недоступна, другие компоненты системы, имеющие отдельные базы данных, продолжат свою работу без сбоев. - -Это существенно упрощает доработку, тестирование и деплой таких систем. - -Использование микросервисов дает большую гибкость в выборе -технологий. Нам не нужно ограничивать себя в использовании -технологий для того, чтобы все стандартизировать внутри -одной большой системы. Работая с отдельными микросервисами, -мы можем выбрать идеально подходящие технологии для отдельных задач. -Например, один микросервис может использовать реляционные -базы данных, а второй эффективнее решает свою задачу -используя NoSQL базу данных или in memory базу данных. - -Еще одно важное преимущество, которое дает микросервисная -архитектура это возможность легко масштабировать систему горизонтально. - -Предположим, что банк испытывает быстрый рост обращений -об открытии новых счетов. - -Если банк использует монолитную архитектуру, -то есть одно большое приложение, ему может потребоваться -запускать отдельный экземпляр всего своего приложения -и направлять часть обращений от клиентов на этот экземпляр (instance). - -Если же банк использует микросервисную архитектуру, -он может запустить несколько экземпляров только сервиса, -ответственного за открытие новых счетов и распределять -нагрузку между этими экземплярами. -При этом микросервис, ответственный за выдачу кредитов, -который не испытывает повышенной нагрузки, может остаться -в одном экземпляре. При этом увеличение числа микросервисов, -ответственных за открытие счетов может происходить автоматически -с помощью инструментов DevOps и Kubernetes. - -Однако использование микросервисов также создает сложности. -Разработка систем, построенных на микросервисной архитектуре -на порядок сложнее. Обслуживание таких систем также -существенно сложнее. - -Компания должна обслуживать множество серверов, -обеспечивать мониторинг каждого микросервиса и так далее. -Также security таких систем существенно сложнее, -поскольку мы имеем дело с множеством приложений, -коммуницирующих друг с другом и все эти коммуникации -должны быть безопасными. - - -Следующий, готовый стать самым популярным -после TopJava курс - [Микросервисы](https://javaops.ru/view/cloudjava). - -[Первое занятие открытое](https://javaops.ru/view/cloudjava/lesson01) diff --git a/doc/video2.4.md b/doc/video2.4.md deleted file mode 100644 index 14041e795564..000000000000 --- a/doc/video2.4.md +++ /dev/null @@ -1,24 +0,0 @@ -## 2.4 Обзор наиболее востребованных технологий. Обзор разрабатываемого приложения - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1LHI18LZK1MRIEBpVe3WjCE890EydN5Gz) - -### О работе с фронтендом и JavaScript -Курс включает себя минимальную практику работы с JavaScript. -Почему это нужно? -В вакансиях backend разработчиков, как правило, отсутствуют -требования глубокого знания JavaScript, но каждый разработчик -должен уметь пользовать DevTools - инструментами разработчика -в браузере, чтобы при работе над рабочими задачами по меньшей -мере иметь возможность понять, происходит ли ошибка на стороне -фронтенда или на стороне бэкенда, увидеть, какие данные уходят -на сервер с фронтенда и так далее. - -Также, конечно, минимальные знания JavaScript приятны, поскольку -они дают вам возможность написать свой несложный проект хотя -бы с минимальным фронтендом. - -Существуют также fullstack-разработчики, от которых требуется -способность полноценно решать задачи фронтенда на продакшене. -Это требует довольно глубокого знания фронтенд-технологий и -таких фреймворков, как Angular или React. Это требует много -времени, и такая подготовка не входит в курс TopJava. \ No newline at end of file diff --git a/doc/video3.md b/doc/video3.md deleted file mode 100644 index 1821aac3f3af..000000000000 --- a/doc/video3.md +++ /dev/null @@ -1,168 +0,0 @@ -## Рекомендуемые подходы обучения на курсе - -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1v5sVL8ivNvSXEPVlrYibFLD5byywRkmT) - -В предыдущих уроках мы сделали обзор технологий, -которые будут использоваться в нашем курсе и создаваемом проекте. -Теперь давайте поговорим о том, как начать -изучать эти технологии и практиковаться в их использовании. - ---- -В 1980 году National Training Laboratories в -США провели исследования эффективности разных -способов обучения. -Выяснилось, что у лекций и чтения книг крайне -низкая эффективность — всего 5-10%. -Дальше идет просмотр видео лекций и прослушивание аудио. - -Максимальная эффективность в 90% — это обучение -людьми других людей — менторинг и немедленное -применение полученных знаний на практике. - - - - - -Я хочу подчеркнуть этот момент. - ->Когда вы смотрите видео, вам может казаться, что вы все понимаете, но, поверьте, когда вы попытаетесь повторить это самостоятельно, у вас возникнет множество вопросов и сложностей. Практика - это важнейшая часть обучения, не пропускайте ее. - -На нашем курсе мы: -- обсуждаем занятия с коллегами и преподавателями в Slack - эффективность 50% -- выполняем практические домашние задания по каждой пройденной теме - 75% -- помогаем коллегам и разрабатываем собственный выпускной проект - 90% - -### О проверке домашних заданий -Также очень важная часть обучения - проверка -ваших домашних заданий и ревью выпускного -проекта нашими кураторами. - ->**Это самый эффективный способ научиться программировать!** - -При устройстве на работу, на собеседовании обязательно -задавай вопрос про ревью кода. -Если его нет, фирма занимается разработкой -непрофессионально? и рост там будет достаточно -медленным и ограниченным. - -В ревью укажут именно твои ошибки -в стиле, структурах данных, алгоритмах и кодировании. -До вечера вторника участники шлют ссылку -на свой GitHub-репозиторий с домашним -заданием занятия, проверка делается -ассистентами, результат пишется в Slack. -Получается эффективно и оперативно. - -После проверки можно исправить замечания -и пройти ее еще раз. - -В конце стажировки делается ревью вашего выпускного проекта. - - -### Участие на стажировке: ожидания и реальность - -Давайте кратко обсудим, чем курс TopJava является -и чем он не является, что следует от него ожидать -и чего не следует. - -#### 1-й тип ложного представления о стажировке: - ->Я увижу, как с нуля строится web-приложение A с использованием технологий B, просмотрю видео по темам, этого будет достаточно - - - -Почему такой подход не верный: -TopJava - это стажировка, поэтому НЕ рассчитывайте -пройти ее на диване с пакетом поп-корна. -Тебе придется на ней РАБОТАТЬ (выполнять ДЗ, -самостоятельно решать какие-то задачи, читать логи, -дебажить, ходить на StackOverflow и даже думать -об этом, засыпая) - -#### 2-й тип ложного представления о стажировке: ->Меня научат шаблонам работы с технологией А, и я -> смогу их применять в любой ситуации - -Почему это тоже не вполне верный подход: -Используемые на стажировке технологии представляют -собой инструменты, которые позволяет сделать -что-то проще. Мы поделимся практикой их использования, -неочевидными особенностями и т. п., покажем -"грабли", на которые вы рано или поздно наступите. -Нет гарантии, что, устроившись на работу, вы -увидите точно такие же подходы. -Все проекты и команды индивидуальны: используются -различные инструменты и различные решения. -Столкновение с технологиями, с которыми -ты ранее не был знаком, - это нормальная -часть жизни любого программиста. Нужно быть к этому готовым. - -Хорошее представление о решении проблем -дает поиск на StackOverflow, где почти -на любую проблему дается большое количество -вариантов решения. Поиск решений и выбор -лучшего - это основная работа Java-разработчика, -и мы максимально постараемся этому научить: -каждый раз в конкретной ситуации вы должны -будете САМИ думать, что применять и как. -Выполняя домашние задания, вы должны приложить -все усилия, чтобы самостоятельно найти решение. -Далее в начале следующего занятия вы также -посмотрите разбор решения, подготовленного -Григорием Кислиным. - -#### 3-й вариант ошибочного представления о стажировке TopJava связан с неверным представлением о том зачем нужна проверка домашних заданий. Например, человек может ошибочно рассуждать так: - ->я хочу проходить стажировку с проверкой ДЗ, -> чтобы мне рассказали, как нужно правильно выполнять задания - -Задача проверки не в том, чтобы общими усилиями -написать код, который ты и так увидишь в разборе. -Если у тебя что-то не получается, наша -задача - не найти ошибку/подебажить за тебя/почитать -логи и т. п., а подсказать способ самостоятельно -найти решение (хотя для этого мы сначала сами -ищем/дебажим/читаем) -Вторая важная задача, которую решает -проверяющий, - увидеть то, что не увидел ты. -Когда ты сдаешь задание на ревью, тебе -может казаться, что все почти идеально. Проверяющий -подскажет тебе, где ты что-то пропустил из-за -недостатка опыта, что позволит тебе улучшить код. - ---- - ->Любое знание стоит воспринимать как подобие семантического дерева: убедитесь в том, что понимаете фундаментальные принципы, то есть ствол и крупные ветки, прежде чем лезть в мелкие листья-детали. Иначе последним не на чем будет держаться -— Илон Маск - -Обычно в занятии дается много дополнительного -материала и ссылок. Не стоит стремиться прочитать -все ссылки урока, их можно использовать как -справочник. Гораздо важнее пройти основной -материал урока и сделать домашнее -задание - этого достаточно для усвоения -материала и получения той самой -основы - ствола и крупных веток, на -которых впоследствии можно наращивать листву. - - -Как правило, подбираются участники разного -уровня. Поэтому главное – не стеснятся -задавать вопросы (после самостоятельного -гугления и поиска решения). Всегда есть -поддержка группы (в том числе от пришедших -на бесплатный повтор участников), моя и ассистентов. - ---- -### Основные навыки программиста, которые необходимо развить на курсе -Давайте перечислим набор навыков, которые -вам необходимо развивать в ходе курса -и которые необходимы любому программисту: - -- умение и привычка искать -информацию, чтобы иметь больший выбор из -доступных вариантов технологий и подходов -к решению задачи, умение пользоваться StackOverflow; -- умение пользоваться дебаггером в Intellij Idea; -- умение пользоваться DevTools в браузере; -- определенный кругозор и опыт для того, чтобы придумывать поисковые запросы. diff --git a/doc/video4.md b/doc/video4.md deleted file mode 100644 index 7d9b00716dca..000000000000 --- a/doc/video4.md +++ /dev/null @@ -1,110 +0,0 @@ -## Структура приложения (многоуровневая архитектура) -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1UHzSy9i-uonmTMFoR5v69Y-vyWLCLQWd) - -Приложение, которое мы будем разрабатывать, это [программа для подсчета калорий](http://javaops-demo.ru/topjava). - -В этом видео обсудим структуру этого приложения. - ---- -Ссылки на отчеты, которые будут использоваться в этом уроке: - -- [Многоуровневая архитектура (русскоязыная статья в Wikipedia)](https://ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%BE%D0%B3%D0%BE%D1%83%D1%80%D0%BE%D0%B2%D0%BD%D0%B5%D0%B2%D0%B0%D1%8F_%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0) -- [Multitier architecture (англоязычная статья в Wikipedia)](https://en.wikipedia.org/wiki/Multitier_architecture) - ---- - - - -На структурной схеме приложения вы видите, что оно условно разделено на 4 части: **Views**, -**Controller**, **Service** и **Repository**. - -Такой подход является реализацией многоуровневой архитектуры в программировании. -По-английски этот подход называется **_Multitier architecture_**. - -Его суть заключается в разделении приложения на несколько слоев, -каждый из которых ответственен за конкретную задачу. - -### Слой отображения (View) -Views соответствует слою отображения (или presentation layer). Это user interface (UI) или -фронтенд - все то, что мы видим и с чем взаимодействуем в браузере. -В качестве View могут быть HTML-страницы, созданные с использованием специальных -движков шаблонов, например, JSP (встроен в Tomcat), или -Thymeleaf (шаблоны по умолчанию в Spring Boot), или отдельное frontend-приложение, -написанное на одном из JavaScript-фреймворков. - -В случае с движками шаблонов HTML-страницы будут располагаться в одном проекте -с основным кодом приложения. Для приложений с "небогатым" UI используются именно шаблоны. -Этот подход отличается от так называемых RIA - rich internet application - приложений со сложным UI. - -Главное отличие rich internet application от приложений с фронтендом -на движках шаблонов заключается в том, что фронтенд, созданный на движке -шаблонов, работает на сервере, и это что-то простое. -В случае с rich internet application фронтенд-приложение загружается -через Интернет к вам на компьютер и запускается в браузере. -Оно может быть максимально сложным и выполнять функции традиционных -десктоп-приложений. -Иногда в одном приложении смешиваются оба способа: например, страница -логина-пароля в RIA делаются на шаблонах. - -Создание простого фронтенда на движке шаблонов проще, поэтому -в курсе мы будем использовать этот способ. - -### Слой контроллеров (Controller) -Следующий слой, который мы видим - это **Controller**. - -В многоуровневой архитектуре он соответствует слою, который -называется "**_Слой приложения_**" или "**_Application layer_**"). -В GRASP (General Responsibility Assignment Software Patterns) -он так и называется - Controller layer. - -Это слой приложения, который ответственен за обработку HTTP-запросов и проверку корректности входных данных. Если мы открываем главную страницу на сайте или отправляем заполненную на сайте форму, фронтенд-приложение отправляет HTTP-запрос серверу (в нашем случае контейнеру сервлетов), который принимает запрос и перенаправляет его в контроллер, соответствующий введенному в браузере URL или адресу, который вызывается при отправке формы через сайт. -Также контроллеры могут принимать запросы не только от фронтенда, но и от других приложений. - -Слой контроллеров не имеет доступа к базе данных. Контроллеры общаются только с сервисами. - -### Слой сервисов (Service layer) -Слой **Service** на схеме приложения соответствует слою -бизнес-логики (или **_Business layer_**) в многоуровневой архитектуре. -В слое Service инкапсулирована вся бизнес-логика нашего приложения. -Если коммуникация с фронтендом или другими приложениями - это ответственность контроллеров, -то обработка данных - это ответственность сервисов. - -### Слой доступа к данным (Data layer) -Слой сервисов общается со слоем, ответственным за работу с базами данных. -Этот слой называют **Data layer** (также можно встретить -названия **Persistence Layer** или **Data access layer**), и он -представлен в виде **_Data Access Object_** классов -(коротко - **_DAO-классы_**) или классов, реализующих паттерн "репозиторий". -С обоими видами классов вы попрактикуетесь в ходе курса. -[Репозиторий - это также один из архитектурных паттернов](https://martinfowler.com/eaaCatalog/repository.html) - -Подобное разделение приложения на слои дает гибкость -и существенно упрощает доработку и переиспользование приложения. -Например, создав по такому принципу приложение, -содержащее слои репозиториев, сервисов и контроллеров, -мы в дальнейшем можем легко использовать это приложение -с различными фронтенд-приложениями или мобильными приложениями. -Если мы решим перейти на другую базу данных, мы можем -переписать только слой репозиториев, и нам не требуется -вносить изменения в слои сервисов и контроллеров. - ->Для маленького приложение такой подход может показаться ->избыточно сложным, но по мере расширения это является спасением. - -Подавляющее большинство реальных приложений построено с использованием именно этой архитектуры. - -### Краткие итоги -В этом видео мы познакомились с концепцией многоуровневой архитектуры, -которую мы применим при создании приложения на курсе. -Многоуровневая архитектура предполагает разделение приложения на слои: -- Views - слой отображения является фронтендом; -- Controller - слой приложения ответственен за прием и валидацию входных данных; -- слой Service включает в себя весь код, отражающий бизнес логику; -- Repository или Data layer отвечает за взаимодействие с базой данных. - -Также мы обсудили какие преимущества дает такой подход. -Среди преимуществ в первую очередь возможность повторного -использования различных слоев и упрощение их доработки и изменения. - - - diff --git a/doc/video5-vcs-git.md b/doc/video5-vcs-git.md deleted file mode 100644 index 7a172e517bc8..000000000000 --- a/doc/video5-vcs-git.md +++ /dev/null @@ -1,180 +0,0 @@ -## Системы управления версиями, Git -## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) [Видео](https://drive.google.com/file/d/1uFjIsxsaSAXxFSwSpjJIGK7Ug2VXf6yH) - -video5-vcs-git.md - -В этом уроке мы рассмотрим системы управления версиями -и самую популярную из них - Git. - ---- -* [StackOverflow 2021 survey](https://insights.stackoverflow.com/survey/2021#technology-most-popular-technologies) -* [Введение в Git и GitHub: установка и настройка](https://topjava.ru/blog/vvedeniye-v-git-github-ustanovka-i-nastroyka) -* [Введение в Git и GitHub: базовые команды](https://topjava.ru/blog/vvedeniye-v-git-github-bazovyye-komandy) -* [Введение в Git и GitHub: ошибки использования](https://topjava.ru/blog/vvedeniye-v-git-oshibki-ispolzovaniya-ch-9) -* [Бесплатная русскоязычная книга Pro Git](https://git-scm.com/book/ru/v2/) ---- - -Git является де-факто стандартом среди систем управления версиями. -Опрос, проведенный StackOverflow в 2021 году показывает, -что Git используют почти 95% опрошенных разработчиков. - -Когда-то популярная централизованная система контроля -версий SVN (Subversion) практически полностью заменена Git. -Но некоторые большие проекты все еще используют SVN. -Примером такого проекта является WordPress. -Я также все еще иногда встречаю SVN в вакансиях российских компаний. - -### Что такое Git и GitHub -В ходе курса мы будем использовать Git. -Git - это распределенная система управления версиями. -Это означает, что код, над которым работает команда, -и ранее сохраненные (закоммиченные) версии проекта -хранится на компьютерах каждого члена команды, -а также в удаленном репозитории, который можно -сравнить с облачным хранилищем. - -Некоторые путают Git и GitHub, но это не одно и то же. - -**Git** — это утилита, которую -программист устанавливает у себя на компьютере для -сохранения состояний проектов и контроля версий проекта. - -**GitHub** — это провайдер удаленных репозиториев, -сайт (хостинг) для хранения кода проекта и его изменений, -для обмена файлами с членами команды проекта. -Программисты могут создавать на GitHub публичные репозитории, -в которых код доступен всем. Компании могут приобрести платный -аккаунт на GitHub и вести свои проекты в закрытых репозиториях, -доступ к которым имеют только члены команды проекта. -Существуют и другие подобные сайты - провайдеры удаленных -репозиториев, например, BitBucket, SourceForge, GitLab и т. д. - - -### Как работают Git и GitHub - - -Говоря кратко, работа с Git и удаленным репозиторием -(в нашем случае это Github) выглядит следующим образом. - -Первый разработчик, который начинал работу над проектом, -использовал команду **_git init_** в корневой директории -проекта для того, чтобы инициализировать пустой Git-репозиторий. -В этот момент Git создает в директории с проектом скрытую -директорию, содержащую файлы, необходимые для его работы. -Теперь первый разработчик может с помощью команды **_git add_** -добавлять файлы проекта в индекс - то есть в зону, -отслеживаемую git, а также может фиксировать изменения, -создавая коммиты (**_git commit_**), и загружать их в -удаленный репозиторий (например, на GitHub) с помощью -команды **_git push_**. - -Спустя некоторое время к работе над проектом подключается второй разработчик. - -Поскольку первый разработчик пушил изменения файлов проекта -на удаленный репозиторий, второй разработчик может скачать -их себе на компьютер с помощью команды **_git pull_** -и продолжить работу с учетом этих изменений. - -Давайте повторим. -В описанной работе использовались команды: - -* **_git init_** - эта команда инициализирует работу Git -для конкретного проекта. В папке с проектом создается -скрытая папка, хранящая все файлы, необходимые Git -для работы. Папка с этими файлами впоследствии будет -загружена в удаленный репозиторий, и второй программист, -скачавший проект, увидит у себя на компьютере все ранее -созданные с помощью Git версии. -* **_git add_** - это команда, которая добавляет файлы -проекта в индекс (или отслеживаемую зону), чтобы их можно -было в какой-то момент закоммитить (то есть сохранить, зафиксировать). -* **_git commit_** - сохраняет все файлы проекта в -текущем состоянии так, что мы в любой момент сможем -вернуться к этому состоянию - посмотреть, как изменились -файлы, или даже полностью вернуть проект к состоянию -данного коммита. -* **_git push_** - загружает файлы в удаленный Git-репозиторий, -чтобы их могли скачать другие программисты. -* **_git pull_** используют для того, чтобы скачать -файлы из удаленного репозитория себе на компьютер. - -Также Git позволяет создавать ветки. Это означает, -что вы можете создать копию стабильного кода и -продолжить работу в этой копии, не подвергая риску -стабильную версию. Когда код с новой функциональностью -готов, отлажен и протестирован, он может быть -слит ("смержен", от слова "merge") в основную ветку со стабильным кодом. - -С помощью этих функций Git позволяет команде разработчиков -работать одновременно над разными задачами и поддерживать -код в стабильном состоянии. - -Также Git позволяет в любой момент понять, кто и когда внес -изменения в определенный код, что существенно упрощает взаимодействия -с коллегами. Например, в случае, если вы видите новые -строки кода, которые вызывают у вас вопросы, с помощью -Git вы можете разобраться, кто внес изменения именно -в эти строки, и связаться с этим человеком -для уточнения деталей. - -Если произошла ситуация, при которой два разработчика -изменили один и тот же файл, Git потребует от вас вручную -отрегулировать такой конфликт. - -Также благодаря Git у вас всегда под рукой детальная -история изменений. С помощью комментариев к коммитам -вы можете детально документировать ход работы над -проектом и в случае необходимости разобраться, в -какой момент что-то пошло не так, и откатиться -к той версии, где ошибка еще не была сделана. -Или просто выяснить, как выглядел код определенного -класса или метода в определенной версии и внести правки -в текущую версию, если это необходимо. - -### Дополнительные материалы - -Для изучения Git рекомендуем в первую очередь ознакомиться со статьями на сайте [topjava.ru](https://topjava.ru): - -* [Введение в Git и GitHub: установка и настройка](https://topjava.ru/blog/vvedeniye-v-git-github-ustanovka-i-nastroyka) -* [Введение в Git и GitHub: базовые команды](https://topjava.ru/blog/vvedeniye-v-git-github-bazovyye-komandy) -* [Введение в Git и GitHub: ошибки использования](https://topjava.ru/blog/vvedeniye-v-git-oshibki-ispolzovaniya-ch-9) - -В них дается детальная инструкция по первоначальной настройке -Git, GitHub, в том числе инструкция по настройке -Access token в GitHub, что вам обязательно нужно будет сделать. - -Также можно упомянуть официальную русскоязычную книгу Pro Git, -которая доступна [бесплатно в электронном виде](https://git-scm.com/book/ru/v2/). - -### Как мы будем использовать Git на курсе -В Git очень много команд, но в ходе курса мы будем использовать -только самые необходимые из них. -С git можно работать как через терминал с помощью команд, -так и с помощью различных программ с графическим интерфейсом. -Мы будем преимущественно использовать интеграцию -IntelliJ Idea с Git, поскольку она очень удобна -и существенно упрощает работу. -Однако знать, как выполнять аналогичные операции -в терминале, очень полезно. - -### Резюме -В этом уроке мы сделали краткий обзор систем управления -версиями и самой популярной из них - Git: - -* разобрались в различиях Git и GitHub. -Git - это система контроля версий, а -GitHub - провайдер удаленных репозиториев, один из многих, доступных на рынке. -* кратко познакомились с тем, как Git работает. -Узнали о командах git init, git add, git commit, git push и git pull. -* Также мы обсудили, что такое ветки и какие возможности они дают. - ---- - -Если вы не работали раньше с Git, не беспокойтесь. -В последующих уроках работа с Git будет изучаться -на практике в ходе работы над проектом. Но дополнительно -почитать о Git все же стоит. - - - - diff --git a/graduation.md b/graduation.md deleted file mode 100644 index a2192f1845c8..000000000000 --- a/graduation.md +++ /dev/null @@ -1,172 +0,0 @@ -## Выпускной проект [стажировки TopJava](https://javaops.ru/view/topjava) - -Design and implement a REST API using Spring-Boot/Spring Data JPA **without frontend**. - -The task is: - -Build a voting system for deciding where to have lunch. - - * 2 types of users: admin and regular users - * Admin can input a restaurant and it's lunch menu of the day (2-5 items usually, just a dish name and price) - * Menu changes each day (admins do the updates) - * Users can vote for a restaurant they want to have lunch at today - * Only one vote counted per user - * If user votes again the same day: - - 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 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 (**better - link to Swagger**). - ------------------------------ -P.S.: Make sure everything works with latest version that is on github :) -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 - сделай все МАКСИМАЛЬНО в этом - стиле. Если тебе кажется, что есть лучшее решение, чем в TopJava - пишите мне в личку, я всегда открыт для улучшений. -- **Рекомендую писать проект на востребованном на рынке стеке**: Spring Boot + Spring Data JPA (работа с БД) + Swagger/OpenAPI 3.0 (REST документация). -- **Оптимально подойдет код миграции TopJava на Spring Boot в конце стажировки или [курса BootJava](https://javaops.ru/view/bootjava)** - -*Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас, конечно, есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. -НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все строго по ТЗ, максимально просто, удобно для доработок и для использования со стороны клиента.* - -> Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять - -_Антуан де Сент-Экзюпери_ - -### 1: ТЗ (Тех.задание) - -- 1.1: Читай ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять -- 1.2: Учитывай, что пользователей может быть ОООЧЕНЬ много, а админов - МАЛО -- 1.3: Сначала сделай основные сценарии по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом. - -### 2. API - -- 2.1: API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (клиента, UI) -- 2.1: Тщательно считайте количество запросов в вашем API для отображения нужной информации -- 2.3: Из потребностей приложения (клиента) реализуй только очевидные сценарии. Необходимо и достаточно: ВСЕ НЕОБХОДИМОЕ для клиента и НИЧЕГО ЛИШНЕГО. Процесс творческий, приходит с опытом. -- 2.4: Делаем REST API в соответствии с концепцией REST (url в общем имеют вид`{ресурс}/{id_ресурсa}[/{подресурс}/{id_подресурсa}][параметры]`, см. ниже ссылки про REST и иерархию). Имена ресурсов во множественном числе! -Самая распространенная и грубая ошибка - не придерживаться этих простых правил. - - **[15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/)** - - **[10 Best Practices for Better RESTful API](https://medium.com/@mwaysolutions/10-best-practices-for-better-restful-api-cbe81b06f291)** - - **[REST resource hierarchy](https://stackoverflow.com/questions/20951419/what-are-best-practices-for-rest-nested-resources)** - - [Лучшие практики разработки REST API: правила 1-7,15-17](https://tproger.ru/translations/luchshie-praktiki-razrabotki-rest-api-20-sovetov/) -- 2.5: Разделение на роли я предпочитаю на уровне URL. Сразу и однозначно видно, какой API у админа, какое у пользователя (API админа начинается, например, с */admin/...*). -- 2.6: На управление (CRUD) ресторанами и едой должны быть ОТДЕЛЬНЫЕ контроллеры. Не надо все, что может админ, сваливать в одну кучу! Смотрите на результат операции - помещаете в этот контроллер! -- 2.7: Проверьте в Swagger, что в POST и PUT нет ничего лишнего, а в GET есть все необходимые данные. Например, при запросе голоса должен в ответе отображаться `id` ресторана, а не весь объект, при создании-редактировании ресторана в примерах swagger не должно быть меню и еды. -- 2.8: `Profile` означает, что данные принадлежат профилю пользователя. Все остальное называйте по-другому. -- 2.9: Отсутствие данных часто бывает "бизнес кейсом", те НЕ ошибкой в запросе или приложении. Исключение - это ошибка, например неверный `id`. Запрос на данные, которые могут быть, могут нет, не должен приводить к исключениям. Посмотрите в сторону `ResponseEntity.of()` -- 2.10: По REST URL должно быть однозначно понятно, какие будут параметры на входе и что ждать на выходе. Без сюрпризов! - -### 3: Код: -- 3.1: Строго соблюдайте соглашения Java по именованию: пакеты ТОЛЬКО маленькими буквами, методы начинаются с маленькой буквы, классы с большой. Незнания Java Core - тестовое задание сразу в корзину. -- 3.2 В проекте (и тестовом задании на работу), в отличие от нашего учебного topjava, оставляйте только необходимый для работы по ТЗ приложения код, ничего лишнего - - 3.2.1: НЕ надо делать разные профили базы и работы с ней - - 3.2.2: НЕ надо делать абстрактных контроллеров на всякий случай - - 3.2.3: НЕ надо делать сервисов, если там нет ничего, кроме делегирования - - 3.2.4: НЕ нужны локализация, UI, типы ошибок, Json View -- 3.3: Название пакетов, имен классов для `model/to/web` стандартные (например `model/domain`). НЕ надо придумывать своих собственных правил -- 3.4: Проверьте, не торчат ли из кода учебные уши TopJava, типа `ProfileRestController.testUTF()`, `AbstractServiceTest.printResult()` или закомментированные `JdbcTemplate`. Назначение выпускного - совсем другое -- 3.5: Вместо `return ResponseEntity.ok(object)` в контроллерах пишите `return object`. Проще! - -### 4: Модель - -- 4.1: В БД обычно хранятся все введенные пользователем и админом данные c возможность их редактирования. Это означает, что мы не удаляем прошлые меню, а храним их в базе, как и историю голосования. Есть базовые вещи, которые закладываются в архитектуру приложения, и есть неочевидные доработки к ТЗ, делать не надо. -- 4.2: Не делайте в модели объектов, которые не будут использоваться в коде (например, не надо двунаправленных связей, если достаточно однонаправленных) -- 4.3: еще раз про [hashCode/equals в Entity](https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma): не делайте в модели сравнение по полям! -- 4.4: ORM работает с объектами. [Иногда, для упрощения логики, fk_id как поля допустимы](https://stackoverflow.com/questions/6311776/hibernate-foreign-keys-instead-of-entities) - -### 5: Архитектура / pom -- 5.1: Можно: - - или подключить DATA-REST (см.курс [Spring Boot 2.x + HATEOAS](https://javaops.ru/view/bootjava)). Контроллеры генерируются автоматически по репозиториям, требуется настройка ресурсов в кастомных контроллерах - - **РЕКОМЕНДУЕТСЯ** делать на основе миграции TopJava / кода [курса BootJava](https://javaops.ru/view/bootjava) - -Нельзя смешивать эти подходы вместе! Я рекомендую 2-й вариант, без data-rest. Обязательно посмотрите в Swagger, какие контроллеры получились в результате. -- 5.2: Не размещайте бизнес-логику приложения и преобразования в TO в слое доступа к DB -- 5.3: Не смешивайте TO и Entity вместе. Они должны быть независимыми друг от друга. На TopJava мы смотрели разные варианты [c использованием TO и без](https://stackoverflow.com/a/21569720/548473). - Делаем максимально просто. -- 5.4: [Use for money in java app](http://stackoverflow.com/a/43051227/548473) -- 5.5 Не надо явно указывать версии зависимостей в `pom`, если они наследуются от `spring-boot-starter-parent` - -### 6: Доступ к БД - -- 6.1: Используйте Spring Data JPA (без лишней делегации). Методы Repository можно вызывать напрямую из сервиса или из контроллера. -- 6.2: В DATA-JPA 2.x используются `Optional`. Попробуйте работать с ними, это безопасный способ работать с null-значениями (используйте `orElseThrow`) -- 6.3: Не делайте при обновлении записи ради экономии пары строчек кода так: -``` -if(updateCondition) - repository.delete(entity) -} -repository.save(entity) -``` -Обновление записи базы должно быть через `UPDATE`. - -### 7: База Данных - -- 7.1: Берите без установки (H2 или HSQLDB). Одну и **в памяти**! Ваше приложение должно сразу запуститься, без всяких настроек и переменных окружения -- 7.2: Тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу -- 7.3: Сделайте [индексы к таблицам](https://ru.wikipedia.org/wiki/Индекс_(базы_данных)). Попробуйте обеспечить UNIQUE (один голос пользователя в день, один уникальный пункт меню в день). -Следите за **порядком полей в индексе**, от этого зависит индексирование запросов. -- 7.4: При популировании добавь записи за сегодняшний день - `now()`, чтобы всегда были актуальные исходные данные -- 7.5: Поля базы case insensitive, не пишите camelStyle (для которых нужны кавычки) -- 7.6: Таблицы обычно именуются в единственном числе. Исключение - `users`, `orders` и другие зарезервированные слова -- 7.7: `date`/`timestamp` - зарезервированное слово, лучше избегать их при именовании полей - -### 8: Security - -- 8.1: Проверьте, станет ли код проще с `@AuthenticationPrincipal` (урок 11, доступ к AuthorizedUser). -- 8.2: Я предпочитаю четкое разделение ролей на основе URL. Для админа URL содержит `/admin` -- 8.3: Еще раз - призываю не менять код TopJava - -### 9: Кэширование - -- 9.1: Кэширование желательно для частых и редко меняющихся запросов от пользователей. -Тщательно продумайте, что надо кэшировать (самые частые запросы), а что нет (большие или редко запрашиваемые данные) -- 9.2: Проверьте соответствие ключей к кэшу (параметры кэшируемого метода) с конфигурацией (например в singleNonExpiryCache, heap=1 в кэше может содержаться только ОДНО значение). - -### 10: Валидация - -- 10.1: Одних аннотаций валидации на полях недостаточно. Должны быть `@Valid/@Validation` в контроллере -- 10.2: Прячте `id` при `create/update` в примерах Swagger. Если их передали - проверяйте на соответствие (в TopJava это `ValidationUtil.checkNew()/assureIdConsistent()`) - -### 11: Дополнительно - -- 11.1: По возможности сделать JUnit-тесты. Можно не делать 100% покрытие, только основные сценарии -- 11.2: Уделяйте внимание обработке ошибок. - -### 12: `readme.md`: - -- 12.1: Поместите вначале `readme` ТЗ или **ссылку на него** - будет понятно о чем твой проект -- 12.2: Если задание на English, описание пиши также на English (то же самое относится к языку резюме: вакансия на English предполагает резюме на English) -- 12.3: Требуемые примеры `curl` не прячьте, а пишите здесь! Оптимально сделать **ссылку на `Swagger UI` с креденшелами для выполнения запросов** -- 12.4: Проверяют люди с опытом в Java: не надо писать инструкций, как устанавливать Java и Maven - -### 13: Git - -- 13.1: Должна быть история ваших комитов с внятными комментариями. Это смотрят. -> Эйчар обращает внимание на дату создания аккаунта и то, как в него заливались коммиты (элементы проекта), постепенно или в один день, насколько технологии в аккаунте коррелируют с технологиями в резюме -- 13.2: Не комить служенбые файлы: логи, DB, настройки IDEA и пр., это сразу - уровень Junior. -- 13.3: Все служебные файлы должны быть в `.gitignore` - -## Проверка - -- Попробуй подергать свое API по всем типичным сценариям ТЗ! - - Удобно использовать? Можно сделать проще? Например, чтобы проголосовать за ресторан залогиненному юзеру, достаточно `restorauntId`. - - Сколько раз пришлось его вызвать API для типичного сценария (например посмотреть рестораны с едой на сегодня)? - - Сколько запросов к базе было сделано? Можно ли сократить (например с `FETCH/Graph` или через кэширование)? - - **Еще раз - проверь все запросы в Sawgger, смотри на формат запросов и данные в ответе. Все должно работать, есть все данные и нет ничего лишнего** -- 13.2: API ДОЛЖЕН соответствовать принципам REST (см. ссылки выше) -- 13.3: ОБЯЗАТЕЛЬНО: запустите `mvn test` - ошибок быть не должно -- 13.4: ОБЯЗАТЕЛЬНО: запустите приложение без всяких предварительных настроек (базы, переменных окружения, ..), лучше на другом компьютере. Приложение должно запускаться и работать! - -## Оценка -ТЗ очень скромное, не дает понимания, что хочет получить заказчик, именно поэтому моих комментариев к этому ТЗ больше на 2 порядка. Это реальное тестовое задание на работу. При оценке учитывается отсутствие грубых ляпов, простота и красота решения + его расширяемость, если понадобится дорабатывать приложение. Если практический опыт небольшой, еще раз очень рекомендую держаться как можно ближе к нашему коду на Spring Boot. -Тестовое задание дается для того, чтобы оценить вас, как программиста. Нужны ли вы компании или нет. Это умение спроектировать грамотную модель и API, придерживаясь принципов REST и без своих велосипедов, и ваш чистый красивый код. diff --git a/patches/icon-meal.png b/patches/icon-meal.png new file mode 100644 index 000000000000..b4fc54ad0129 Binary files /dev/null and b/patches/icon-meal.png differ diff --git a/pom.xml b/pom.xml index cf231320e25d..876c3dcf9fb0 100644 --- a/pom.xml +++ b/pom.xml @@ -4,41 +4,376 @@ ru.javawebinar topjava - jar + war 1.0-SNAPSHOT Calories Management - https://javaops-demo.ru/topjava + http://topjava.herokuapp.com/ - 1.8 + 14 UTF-8 UTF-8 + + 5.2.8.RELEASE + 5.3.3.RELEASE + 2.3.2.RELEASE + 2.11.1 + 9.0.36 + + + 1.2.3 + 1.7.30 + + + 42.2.14 + + 5.6.2 + 3.16.1 + 1.3 + + + 5.4.18.Final + 6.1.5.Final + 3.0.1-b11 + + + 3.8.1 + + + 4.5.0 + 3.5.1 + 2.5.20-1 + 3.1.4 + 1.10.21 topjava - install + package org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + 3.8.1 ${java.version} ${java.version} + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + -Dfile.encoding=UTF-8 + + + + + + + org.codehaus.cargo + cargo-maven2-plugin + 1.7.13 + + + 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.springframework.security + spring-security-web + ${spring.security.version} + + + org.springframework.security + spring-security-config + ${spring.security.version} + + + org.springframework.security + spring-security-taglibs + ${spring.security.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 + 2.4.0-b180830.0438 + + + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + + javax.servlet + jstl + 1.2 + + + + org.springframework + spring-webmvc + + + + + org.webjars + jquery + ${webjars-jquery.version} + + + org.webjars + bootstrap + ${webjars-bootstrap.version} + + + org.webjars + jquery + + + + org.webjars + popper.js + + + + + org.webjars + datatables + ${webjars-datatables.version} + + + org.webjars + jquery + + + + + org.webjars + datetimepicker + ${webjars-datetimepicker.version} + + + org.webjars.bower + noty + ${webjars-noty.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-json.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate5 + ${jackson-json.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson-json.version} + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + + org.hamcrest + hamcrest-core + ${hamcrest.version} + test + + + + org.springframework + spring-test + test + + + org.springframework.security + spring-security-test + ${spring.security.version} + 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/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java new file mode 100644 index 000000000000..b4dff02fcf8b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java @@ -0,0 +1,33 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; + +public class AuthorizedUser extends org.springframework.security.core.userdetails.User { + private static final long serialVersionUID = 1L; + + private UserTo userTo; + + public AuthorizedUser(User user) { + super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles()); + this.userTo = UserUtil.asTo(user); + } + + public int getId() { + return userTo.id(); + } + + public void update(UserTo newTo) { + userTo = newTo; + } + + public UserTo getUserTo() { + return userTo; + } + + @Override + public String toString() { + return userTo.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/HasId.java b/src/main/java/ru/javawebinar/topjava/HasId.java new file mode 100644 index 000000000000..2ba30235cb87 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/HasId.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava; + +import org.springframework.util.Assert; + +public interface HasId { + Integer getId(); + + void setId(Integer id); + + default boolean isNew() { + return getId() == null; + } + + // doesn't work for hibernate lazy proxy + default int id() { + Assert.notNull(getId(), "Entity must has id"); + return getId(); + } +} 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 723742bacade..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..d6ef2e0379ed --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava; + +public class Profiles { + public static final String + JDBC = "jdbc", + JPA = "jpa", + DATAJPA = "datajpa"; + + public static final String REPOSITORY_IMPLEMENTATION = DATAJPA; + + public static final String + POSTGRES_DB = "postgres", + HSQL_DB = "hsqldb"; + + // Get DB profile depending of DB driver in classpath + public static String getActiveDbProfile() { + try { + Class.forName("org.postgresql.Driver"); + return POSTGRES_DB; + } catch (ClassNotFoundException ex) { + try { + Class.forName("org.hsqldb.jdbcDriver"); + return Profiles.HSQL_DB; + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Could not find DB driver"); + } + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/View.java b/src/main/java/ru/javawebinar/topjava/View.java new file mode 100644 index 000000000000..62c332bcf970 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/View.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava; + +import javax.validation.groups.Default; + +public class View { + public interface Persist extends Default {} +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java new file mode 100644 index 000000000000..34796b59f8b9 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -0,0 +1,69 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.Hibernate; +import org.springframework.util.Assert; +import ru.javawebinar.topjava.HasId; + +import javax.persistence.*; + +@MappedSuperclass +// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access +@Access(AccessType.FIELD) +//@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE) +public abstract class AbstractBaseEntity implements HasId { + public static final int START_SEQ = 100000; + + @Id + @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ) + // @Column(name = "id", unique = true, nullable = false, columnDefinition = "integer default nextval('global_seq')") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") + +// See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034 +// Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting + protected Integer id; + + protected AbstractBaseEntity() { + } + + protected AbstractBaseEntity(Integer id) { + this.id = id; + } + + @Override + public void setId(Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + + // doesn't work for hibernate lazy proxy + public int id() { + Assert.notNull(id, "Entity must has id"); + return id; + } + + @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..edff2ab1718f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.model; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + + +@MappedSuperclass +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + @NotBlank + @Size(min = 2, max = 100) + @Column(name = "name", nullable = false) + protected String name; + + 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..0efad94fc246 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -0,0 +1,119 @@ +package ru.javawebinar.topjava.model; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.validator.constraints.Range; +import org.springframework.format.annotation.DateTimeFormat; +import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.util.DateTimeUtil; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@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 + @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) + private LocalDateTime dateTime; + + @Column(name = "description", nullable = false) + @NotBlank + @Size(min = 2, max = 120) + private String description; + + @Column(name = "calories", nullable = false) + @NotNull + @Range(min = 10, max = 5000) + private Integer calories; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + @JsonBackReference + @NotNull(groups = View.Persist.class) + private User user; + + public Meal() { + } + + public Meal(LocalDateTime dateTime, String description, int calories) { + this(null, dateTime, description, calories); + } + + public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { + super(id); + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public Integer getCalories() { + return calories; + } + + public LocalDate getDate() { + return dateTime.toLocalDate(); + } + + public LocalTime getTime() { + return dateTime.toLocalTime(); + } + + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setCalories(Integer calories) { + this.calories = calories; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public String toString() { + return "Meal{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java new file mode 100644 index 000000000000..27f3e52311f8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -0,0 +1,14 @@ +package ru.javawebinar.topjava.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum Role implements GrantedAuthority { + USER, + ADMIN; + +// https://stackoverflow.com/a/19542316/548473 + @Override + public String getAuthority() { + return "ROLE_" + name(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java new file mode 100644 index 000000000000..08bf0394bb94 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -0,0 +1,162 @@ +package ru.javawebinar.topjava.model; + +import com.fasterxml.jackson.annotation.JsonManagedReference; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.validator.constraints.Range; +import org.springframework.util.CollectionUtils; + +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.*; + +import static ru.javawebinar.topjava.util.UserUtil.DEFAULT_CALORIES_PER_DAY; + +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@NamedQueries({ + @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"), + @NamedQuery(name = User.BY_EMAIL, query = "SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"), + @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u ORDER BY u.name, u.email"), +}) +@Entity +@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "users_unique_email_idx")}) +public class User extends AbstractNamedEntity { + + 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) + // https://stackoverflow.com/a/12505165/548473 + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String password; + + @Column(name = "enabled", nullable = false, columnDefinition = "bool default true") + private boolean enabled = true; + + @Column(name = "registered", nullable = false, columnDefinition = "timestamp default now()") + @NotNull + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Date registered = new Date(); + + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), + uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "user_roles_unique_idx")}) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) +// @Fetch(FetchMode.SUBSELECT) + @BatchSize(size = 200) + private Set roles; + + @Column(name = "calories_per_day", nullable = false, columnDefinition = "int default 2000") + @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") + @JsonManagedReference + 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, int caloriesPerDay, Role role, Role... roles) { + this(id, name, email, password, caloriesPerDay, true, new Date(), EnumSet.of(role, roles)); + } + + public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) { + super(id, name); + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + this.enabled = enabled; + this.registered = registered; + setRoles(roles); + } + + 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; + } + + public void setMeals(List meals) { + this.meals = 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/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java new file mode 100644 index 000000000000..86beb3fb6ac3 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -0,0 +1,27 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; + +import java.time.LocalDateTime; +import java.util.List; + +public interface MealRepository { + // null if updated meal do not belong to userId + Meal save(Meal meal, int userId); + + // false if meal do not belong to userId + boolean delete(int id, int userId); + + // null if meal do not belong to userId + Meal get(int id, int userId); + + // ORDERED dateTime desc + List getAll(int userId); + + // ORDERED dateTime desc + List 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..692e50c87c77 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/CrudUserRepository.java @@ -0,0 +1,24 @@ +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); + + // https://stackoverflow.com/a/46013654/548473 + @EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD) + @Query("SELECT u FROM User u WHERE u.id=?1") + User getWithMeals(int id); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/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..82c80dd7b913 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -0,0 +1,91 @@ +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 org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; +import ru.javawebinar.topjava.util.ValidationUtil; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@Transactional(readOnly = true) +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 + @Transactional + public Meal save(Meal meal, int userId) { + ValidationUtil.validate(meal); + + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", meal.getId()) + .addValue("description", meal.getDescription()) + .addValue("calories", meal.getCalories()) + .addValue("date_time", meal.getDateTime()) + .addValue("user_id", userId); + + if (meal.isNew()) { + Number newId = insertMeal.executeAndReturnKey(map); + meal.setId(newId.intValue()); + } else { + if (namedParameterJdbcTemplate.update("" + + "UPDATE meals " + + " SET description=:description, calories=:calories, date_time=:date_time " + + " WHERE id=:id AND user_id=:user_id", map) == 0) { + return null; + } + } + return meal; + } + + @Override + @Transactional + public boolean delete(int id, int userId) { + return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0; + } + + @Override + public Meal get(int id, int userId) { + List meals = jdbcTemplate.query( + "SELECT * FROM meals WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId); + return DataAccessUtils.singleResult(meals); + } + + @Override + public List getAll(int userId) { + return jdbcTemplate.query( + "SELECT * FROM meals WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId); + } + + @Override + public List 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..9180eaa1316e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -0,0 +1,121 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.util.ValidationUtil; + +import java.util.*; + +@Repository +@Transactional(readOnly = true) +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 + @Transactional + public User save(User user) { + ValidationUtil.validate(user); + + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user); + + if (user.isNew()) { + Number newKey = insertUser.executeAndReturnKey(parameterSource); + user.setId(newKey.intValue()); + insertRoles(user); + } else { + if (namedParameterJdbcTemplate.update( + "UPDATE users SET name=:name, email=:email, password=:password, " + + "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", parameterSource) == 0) { + return null; + } + // Simplest implementation. + // More complicated : get user roles from DB and compare them with user.roles (assume that roles are changed rarely). + // If roles are changed, calculate difference in java and delete/insert them. + deleteRoles(user); + insertRoles(user); + } + return user; + } + + @Override + @Transactional + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0; + } + + @Override + public User get(int id) { + List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id); + return setRoles(DataAccessUtils.singleResult(users)); + } + + @Override + public User getByEmail(String email) { +// return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email); + return setRoles(DataAccessUtils.singleResult(users)); + } + + @Override + public List getAll() { + Map> map = new HashMap<>(); + jdbcTemplate.query("SELECT * FROM user_roles", rs -> { + map.computeIfAbsent(rs.getInt("user_id"), userId -> EnumSet.noneOf(Role.class)) + .add(Role.valueOf(rs.getString("role"))); + }); + List users = jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + users.forEach(u -> u.setRoles(map.get(u.getId()))); + return users; + } + + private void insertRoles(User u) { + Set roles = u.getRoles(); + if (!CollectionUtils.isEmpty(roles)) { + jdbcTemplate.batchUpdate("INSERT INTO user_roles (user_id, role) VALUES (?, ?)", roles, roles.size(), + (ps, role) -> { + ps.setInt(1, u.getId()); + ps.setString(2, role.name()); + }); + } + } + + private void deleteRoles(User u) { + jdbcTemplate.update("DELETE FROM user_roles WHERE user_id=?", u.getId()); + } + + private User setRoles(User u) { + if (u != null) { + List roles = jdbcTemplate.queryForList("SELECT role FROM user_roles WHERE user_id=?", Role.class, u.getId()); + u.setRoles(roles); + } + return u; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java 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..26c16f7df718 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepository.java @@ -0,0 +1,74 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.hibernate.jpa.QueryHints; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; + +@Repository +@Transactional(readOnly = true) +public class 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) + .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false) + .getResultList(); + return DataAccessUtils.singleResult(users); + } + + @Override + public List getAll() { + return em.createNamedQuery(User.ALL_SORTED, User.class).getResultList(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java new file mode 100644 index 000000000000..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..5481b18447fd --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -0,0 +1,100 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; + +import java.util.List; + +import static ru.javawebinar.topjava.util.UserUtil.prepareToSave; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +@Service("userService") +@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) +public class UserService implements UserDetailsService { + + private final UserRepository repository; + private final PasswordEncoder passwordEncoder; + + public UserService(UserRepository repository, PasswordEncoder passwordEncoder) { + this.repository = repository; + this.passwordEncoder = passwordEncoder; + } + + @CacheEvict(value = "users", allEntries = true) + public User create(User user) { + Assert.notNull(user, "user must not be null"); + return prepareAndSave(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 : check works only for JDBC, disabled + prepareAndSave(user); + } + + @CacheEvict(value = "users", allEntries = true) + @Transactional + public void update(UserTo userTo) { + User user = get(userTo.id()); + prepareAndSave(UserUtil.updateFromTo(user, userTo)); // !! need only for JDBC implementation + } + + @CacheEvict(value = "users", allEntries = true) + @Transactional + public void enable(int id, boolean enabled) { + User user = get(id); + user.setEnabled(enabled); + repository.save(user); // !! need only for JDBC implementation + } + + @Override + public AuthorizedUser loadUserByUsername(String email) throws UsernameNotFoundException { + User user = repository.getByEmail(email.toLowerCase()); + if (user == null) { + throw new UsernameNotFoundException("User " + email + " is not found"); + } + return new AuthorizedUser(user); + } + + private User prepareAndSave(User user) { + return repository.save(prepareToSave(user, passwordEncoder)); + } + + public User getWithMeals(int id) { + return checkNotFoundWithId(repository.getWithMeals(id), id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/to/BaseTo.java b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java new file mode 100644 index 000000000000..b7a7de6b7d66 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.to; + +import ru.javawebinar.topjava.HasId; + +public abstract class BaseTo implements HasId { + protected Integer id; + + public BaseTo() { + } + + public BaseTo(Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + + @Override + public void setId(Integer id) { + this.id = id; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java new file mode 100644 index 000000000000..04a9ebd09cc0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -0,0 +1,69 @@ +package ru.javawebinar.topjava.to; + +import java.beans.ConstructorProperties; +import java.time.LocalDateTime; +import java.util.Objects; + +public class MealTo extends BaseTo { + + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + private final boolean excess; + + @ConstructorProperties({"id", "dateTime", "description", "calories", "excess"}) + public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) { + super(id); + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + this.excess = excess; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public boolean isExcess() { + return excess; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MealTo that = (MealTo) o; + return calories == that.calories && + excess == that.excess && + Objects.equals(id, that.id) && + Objects.equals(dateTime, that.dateTime) && + Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash(id, dateTime, description, calories, excess); + } + + @Override + public String toString() { + return "MealTo{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + ", excess=" + excess + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java new file mode 100644 index 000000000000..2d9d81480291 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -0,0 +1,84 @@ +package ru.javawebinar.topjava.to; + +import org.hibernate.validator.constraints.Range; +import ru.javawebinar.topjava.util.UserUtil; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; + +public class UserTo extends BaseTo implements Serializable { + private static final long serialVersionUID = 1L; + + @NotBlank + @Size(min = 2, max = 100) + private String name; + + @Email + @NotBlank + @Size(max = 100) + private String email; + + @NotBlank + @Size(min = 5, max = 32, message = "length must be between 5 and 32 characters") + private String password; + + @Range(min = 10, max = 10000) + @NotNull + private Integer caloriesPerDay = UserUtil.DEFAULT_CALORIES_PER_DAY; + + public UserTo() { + } + + public UserTo(Integer id, String name, String email, String password, int caloriesPerDay) { + super(id); + this.name = name; + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setCaloriesPerDay(Integer caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public Integer getCaloriesPerDay() { + return caloriesPerDay; + } + + @Override + public String toString() { + return "UserTo{" + + "id=" + id + + ", name='" + name + '\'' + + ", email='" + email + '\'' + + ", caloriesPerDay='" + caloriesPerDay + '\'' + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java new file mode 100644 index 000000000000..62b8baaaf6d8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -0,0 +1,42 @@ +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 { + public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm"; + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); + + // 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.isEmpty(str) ? null : LocalDate.parse(str); + } + + public static @Nullable LocalTime parseLocalTime(@Nullable String str) { + return StringUtils.isEmpty(str) ? null : LocalTime.parse(str); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java new file mode 100644 index 000000000000..8eb2607228a8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -0,0 +1,43 @@ +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 { + + 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()); + } + + public 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/UserUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java new file mode 100644 index 000000000000..f81b1a6603dc --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java @@ -0,0 +1,35 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.StringUtils; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; + +public class UserUtil { + + public static final int DEFAULT_CALORIES_PER_DAY = 2000; + + public static User createNewFromTo(UserTo userTo) { + return new User(null, userTo.getName(), userTo.getEmail().toLowerCase(), userTo.getPassword(), userTo.getCaloriesPerDay(), Role.USER); + } + + public static UserTo asTo(User user) { + return new UserTo(user.getId(), user.getName(), user.getEmail(), user.getPassword(), user.getCaloriesPerDay()); + } + + public static User updateFromTo(User user, UserTo userTo) { + user.setName(userTo.getName()); + user.setEmail(userTo.getEmail().toLowerCase()); + user.setCaloriesPerDay(userTo.getCaloriesPerDay()); + user.setPassword(userTo.getPassword()); + return user; + } + + public static User prepareToSave(User user, PasswordEncoder passwordEncoder) { + String password = user.getPassword(); + user.setPassword(StringUtils.hasText(password) ? passwordEncoder.encode(password) : password); + user.setEmail(user.getEmail().toLowerCase()); + return user; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java new file mode 100644 index 000000000000..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..ac3df486686d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -0,0 +1,87 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import ru.javawebinar.topjava.HasId; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import javax.validation.*; +import java.util.Set; +import java.util.stream.Collectors; + +public class ValidationUtil { + private static final Validator validator; + + static { + // From Javadoc: implementations are thread-safe and instances are typically cached and reused. + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + // From Javadoc: implementations of this interface must be thread-safe + validator = factory.getValidator(); + } + + private ValidationUtil() { + } + + public static void validate(T bean) { + // https://alexkosarev.name/2018/07/30/bean-validation-api/ + Set> violations = validator.validate(bean); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } + + 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(HasId bean) { + if (!bean.isNew()) { + throw new IllegalRequestDataException(bean + " must be new (id=null)"); + } + } + + public static void assureIdConsistent(HasId bean, int id) { +// conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473) + if (bean.isNew()) { + bean.setId(id); + } else if (bean.id() != id) { + throw new IllegalRequestDataException(bean + " must be with id=" + id); + } + } + + // http://stackoverflow.com/a/28565320/548473 + public static Throwable getRootCause(Throwable t) { + Throwable result = t; + Throwable cause; + + while (null != (cause = result.getCause()) && (result != cause)) { + result = cause; + } + return result; + } + + public static ResponseEntity getErrorResponse(BindingResult result) { + return ResponseEntity.unprocessableEntity().body( + result.getFieldErrors().stream() + .map(fe -> String.format("[%s] %s", fe.getField(), fe.getDefaultMessage())) + .collect(Collectors.joining("
")) + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java new file mode 100644 index 000000000000..d43323590d95 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -0,0 +1,13 @@ +package ru.javawebinar.topjava.util.exception; + +public class ErrorInfo { + private final String url; + private final ErrorType type; + private final String detail; + + public ErrorInfo(CharSequence url, ErrorType type, String detail) { + this.url = url.toString(); + this.type = type; + this.detail = detail; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java new file mode 100644 index 000000000000..c53a433bc668 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java @@ -0,0 +1,8 @@ +package ru.javawebinar.topjava.util.exception; + +public enum ErrorType { + APP_ERROR, + DATA_NOT_FOUND, + DATA_ERROR, + VALIDATION_ERROR +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java b/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java new file mode 100644 index 000000000000..2b144f91c91c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util.exception; + +public class IllegalRequestDataException extends RuntimeException { + public IllegalRequestDataException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/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/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java new file mode 100644 index 000000000000..e3dbdf7d7120 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -0,0 +1,65 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ErrorInfo; +import ru.javawebinar.topjava.util.exception.ErrorType; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import javax.servlet.http.HttpServletRequest; + +import static ru.javawebinar.topjava.util.exception.ErrorType.*; + +@RestControllerAdvice(annotations = RestController.class) +@Order(Ordered.HIGHEST_PRECEDENCE + 5) +public class ExceptionInfoHandler { + private static Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); + + // http://stackoverflow.com/a/22358422/548473 + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) + @ExceptionHandler(NotFoundException.class) + public ErrorInfo handleError(HttpServletRequest req, NotFoundException e) { + return logAndGetErrorInfo(req, e, false, DATA_NOT_FOUND); + } + + @ResponseStatus(value = HttpStatus.CONFLICT) // 409 + @ExceptionHandler(DataIntegrityViolationException.class) + public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { + return logAndGetErrorInfo(req, e, true, DATA_ERROR); + } + + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler({IllegalRequestDataException.class, MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class}) + public ErrorInfo illegalRequestDataError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ErrorInfo handleError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, true, APP_ERROR); + } + + // https://stackoverflow.com/questions/538870/should-private-helper-methods-be-static-if-they-can-be-static + private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType) { + Throwable rootCause = ValidationUtil.getRootCause(e); + if (logException) { + log.error(errorType + " at request " + req.getRequestURL(), rootCause); + } else { + log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); + } + return new ErrorInfo(req.getRequestURL(), errorType, rootCause.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java new file mode 100644 index 000000000000..bd401d1c150e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.util.ValidationUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(Exception.class) + public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { + log.error("Exception at request " + req.getRequestURL(), e); + Throwable rootCause = ValidationUtil.getRootCause(e); + + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + ModelAndView mav = new ModelAndView("exception", + Map.of("exception", rootCause, "message", rootCause.toString(), "status", httpStatus)); + mav.setStatus(httpStatus); + + // Interceptor is not invoked, put userTo + AuthorizedUser authorizedUser = SecurityUtil.safeGet(); + if (authorizedUser != null) { + mav.addObject("userTo", authorizedUser.getUserTo()); + } + return mav; + } +} 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..2a4bb187e418 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -0,0 +1,31 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class RootController { + + @GetMapping("/") + public String root() { + return "redirect:meals"; + } + + // @Secured("ROLE_ADMIN") + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/users") + public String getUsers() { + return "users"; + } + + @GetMapping(value = "/login") + public String login() { + return "login"; + } + + @GetMapping("/meals") + public String getMeals() { + return "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..ac5b99f052f1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -0,0 +1,34 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import ru.javawebinar.topjava.AuthorizedUser; + +import static java.util.Objects.requireNonNull; + +public class SecurityUtil { + + private SecurityUtil() { + } + + public static AuthorizedUser safeGet() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null) { + return null; + } + Object principal = auth.getPrincipal(); + return (principal instanceof AuthorizedUser) ? (AuthorizedUser) principal : null; + } + + public static AuthorizedUser get() { + return requireNonNull(safeGet(), "No authorized user found"); + } + + public static int authUserId() { + return get().getUserTo().id(); + } + + public static int authUserCaloriesPerDay() { + return get().getUserTo().getCaloriesPerDay(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java new file mode 100644 index 000000000000..bc4409869cce --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava.web.converter; + +import org.springframework.format.Formatter; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; + +public class DateTimeFormatters { + public static class LocalDateFormatter implements Formatter { + + @Override + public LocalDate parse(String text, Locale locale) { + return parseLocalDate(text); + } + + @Override + public String print(LocalDate lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_DATE); + } + } + + public static class LocalTimeFormatter implements Formatter { + + @Override + public LocalTime parse(String text, Locale locale) { + return parseLocalTime(text); + } + + @Override + public String print(LocalTime lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_TIME); + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java new file mode 100644 index 000000000000..881dfaa4448e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java @@ -0,0 +1,25 @@ +package ru.javawebinar.topjava.web.interceptor; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.web.SecurityUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * This interceptor adds userTo to the model of every requests + */ +public class ModelInterceptor extends HandlerInterceptorAdapter { + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + if (modelAndView != null && !modelAndView.isEmpty()) { + AuthorizedUser authorizedUser = SecurityUtil.safeGet(); + if (authorizedUser != null) { + modelAndView.getModelMap().addAttribute("userTo", authorizedUser.getUserTo()); + } + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java new file mode 100644 index 000000000000..8237df93bffe --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.web.json; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +/** + *

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

    Filter separately + *
  1. by date
  2. + *
  3. by time for every date
  4. + *
+ */ + public List getBetween(@Nullable LocalDate startDate, @Nullable LocalTime startTime, + @Nullable LocalDate endDate, @Nullable LocalTime endTime) { + int userId = SecurityUtil.authUserId(); + log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId); + + List mealsDateFiltered = service.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/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java new file mode 100644 index 000000000000..333b884d37d6 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -0,0 +1,68 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; + +import java.net.URI; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@RestController +@RequestMapping(value = MealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) +public class MealRestController extends AbstractMealController { + static final String REST_URL = "/rest/profile/meals"; + + @Override + @GetMapping("/{id}") + public Meal get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @Override + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void update(@RequestBody Meal meal, @PathVariable int id) { + super.update(meal, id); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@RequestBody Meal meal) { + Meal created = super.create(meal); + + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + + return ResponseEntity.created(uriOfNewResource).body(created); + } + + @Override + @GetMapping(value = "/filter") + public List getBetween( + @RequestParam @Nullable LocalDate startDate, + @RequestParam @Nullable LocalTime startTime, + @RequestParam @Nullable LocalDate endDate, + @RequestParam @Nullable LocalTime endTime) { + return super.getBetween(startDate, startTime, endDate, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java new file mode 100644 index 000000000000..cd1060cc9b31 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java @@ -0,0 +1,64 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.util.ValidationUtil; + +import javax.validation.Valid; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@RestController +@RequestMapping("/profile/meals") +public class MealUIController extends AbstractMealController { + + @Override + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + return super.getAll(); + } + + @Override + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public Meal get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping(value = "/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @PostMapping + public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult result) { + if (result.hasErrors()) { + // TODO change to exception handler + return ValidationUtil.getErrorResponse(result); + } + if (meal.isNew()) { + super.create(meal); + } else { + super.update(meal, meal.getId()); + } + return ResponseEntity.ok().build(); + } + + @Override + @GetMapping(value = "/filter", produces = MediaType.APPLICATION_JSON_VALUE) + public List getBetween( + @RequestParam @Nullable LocalDate startDate, + @RequestParam @Nullable LocalTime startTime, + @RequestParam @Nullable LocalDate endDate, + @RequestParam @Nullable LocalTime endTime) { + return super.getBetween(startDate, startTime, endDate, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java new file mode 100644 index 000000000000..4e4ca290a999 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -0,0 +1,74 @@ +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 ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; + +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(UserTo userTo) { + log.info("create from to {}", userTo); + return create(UserUtil.createNewFromTo(userTo)); + } + + 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 void update(UserTo userTo, int id) { + log.info("update {} with id={}", userTo, id); + assureIdConsistent(userTo, id); + service.update(userTo); + } + + public User getByMail(String email) { + log.info("getByEmail {}", email); + return service.getByEmail(email); + } + + public void enable(int id, boolean enabled) { + log.info(enabled ? "enable {}" : "disable {}", id); + service.enable(id, enabled); + } + + public User getWithMeals(int id) { + log.info("getWithMeals {}", id); + return service.getWithMeals(id); + } +} \ 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..037f370d7bf6 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -0,0 +1,69 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.model.User; + +import java.net.URI; +import java.util.List; + +@RestController +@RequestMapping(value = AdminRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) +public class AdminRestController extends AbstractUserController { + + static final String REST_URL = "/rest/admin/users"; + + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @GetMapping("/{id}") + public User get(@PathVariable int id) { + return super.get(id); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@RequestBody User user) { + User created = super.create(user); + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + return ResponseEntity.created(uriOfNewResource).body(created); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @Override + @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void update(@RequestBody User user, @PathVariable int id) { + super.update(user, id); + } + + @GetMapping("/by") + public User getByMail(@RequestParam String email) { + return super.getByMail(email); + } + + @Override + @PatchMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void enable(@PathVariable int id, @RequestParam boolean enabled) { + super.enable(id, enabled); + } + + @GetMapping("/{id}/with-meals") + public User getWithMeals(@PathVariable int id) { + return super.getWithMeals(id); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java new file mode 100644 index 000000000000..a4d2b923c984 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java @@ -0,0 +1,59 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.ValidationUtil; + +import javax.validation.Valid; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/admin/users") +public class AdminUIController extends AbstractUserController { + + @Override + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + return super.getAll(); + } + + @Override + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public User get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @PostMapping + public ResponseEntity createOrUpdate(@Valid UserTo userTo, BindingResult result) { + if (result.hasErrors()) { + // TODO change to exception handler + return ValidationUtil.getErrorResponse(result); + } + if (userTo.isNew()) { + super.create(userTo); + } else { + super.update(userTo, userTo.id()); + } + return ResponseEntity.ok().build(); + } + + @Override + @PostMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) + public void enable(@PathVariable int id, @RequestParam boolean enabled) { + super.enable(id, enabled); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java new file mode 100644 index 000000000000..416bc0229d58 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -0,0 +1,55 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; + +import java.net.URI; + +import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; + +@RestController +@RequestMapping(ProfileRestController.REST_URL) +public class ProfileRestController extends AbstractUserController { + static final String REST_URL = "/rest/profile"; + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public User get() { + return super.get(authUserId()); + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete() { + super.delete(authUserId()); + } + + @PostMapping(value = "/register", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(value = HttpStatus.CREATED) + public ResponseEntity register(@RequestBody UserTo userTo) { + User created = super.create(userTo); + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL).build().toUri(); + return ResponseEntity.created(uriOfNewResource).body(created); + } + + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void update(@RequestBody UserTo userTo) { + super.update(userTo, authUserId()); + } + + @GetMapping(value = "/text") + public String testUTF() { + return "Русский текст"; + } + + @GetMapping("/with-meals") + public User getWithMeals() { + return super.getWithMeals(authUserId()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java new file mode 100644 index 000000000000..1bcbdd3f66a5 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.support.SessionStatus; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.web.SecurityUtil; + +import javax.validation.Valid; + +@Controller +@RequestMapping("/profile") +public class ProfileUIController extends AbstractUserController { + + @GetMapping + public String profile() { + return "profile"; + } + + @PostMapping + public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status) { + if (result.hasErrors()) { + return "profile"; + } else { + super.update(userTo, SecurityUtil.authUserId()); + SecurityUtil.get().update(userTo); + status.setComplete(); + return "redirect:/meals"; + } + } + + @GetMapping("/register") + public String register(ModelMap model) { + model.addAttribute("userTo", new UserTo()); + model.addAttribute("register", true); + return "profile"; + } + + @PostMapping("/register") + public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) { + if (result.hasErrors()) { + model.addAttribute("register", true); + return "profile"; + } else { + super.create(userTo); + status.setComplete(); + return "redirect:/login?message=app.registered&username=" + userTo.getEmail(); + } + } +} \ 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..c6347bd940d6 --- /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..dd2adc28b266 --- /dev/null +++ b/src/main/resources/db/populateDB.sql @@ -0,0 +1,24 @@ +DELETE FROM user_roles; +DELETE FROM meals; +DELETE FROM users; +ALTER SEQUENCE global_seq RESTART WITH 100000; + +INSERT INTO users (name, email, password, calories_per_day) +VALUES ('User', 'user@yandex.ru', '{noop}password', 2005), + ('Admin', 'admin@gmail.com', '{noop}admin', 1900); + +INSERT INTO user_roles (role, user_id) +VALUES ('USER', 100000), + ('ADMIN', 100001), + ('USER', 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..b68d5b62087f --- /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=false \ No newline at end of file diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties new file mode 100644 index 000000000000..2e073681ad16 --- /dev/null +++ b/src/main/resources/db/tomcat.properties @@ -0,0 +1,5 @@ +database.init=false +jdbc.initLocation=initDB.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 000000000000..ab4cfe51e08a --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,33 @@ + + + + + + + + ${TOPJAVA_ROOT}/log/topjava.log + + + UTF-8 + %date %-5level %logger{50}.%M:%L - %msg%n + + + + + + UTF-8 + %d{HH:mm:ss.SSS} %highlight(%-5level) %cyan(%class{50}.%M:%L) - %msg%n + + + + + + + + + + + + + + diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml new file mode 100644 index 000000000000..3a75ebb82910 --- /dev/null +++ b/src/main/resources/spring/spring-app.xml @@ -0,0 +1,16 @@ + + + + + + + + \ 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..9c090f3c544a --- /dev/null +++ b/src/main/resources/spring/spring-db.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..79ff4dace888 --- /dev/null +++ b/src/main/resources/spring/spring-mvc.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + text/plain;charset=UTF-8 + text/html;charset=UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml new file mode 100644 index 000000000000..e3c3a864c5f8 --- /dev/null +++ b/src/main/resources/spring/spring-security.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/tomcat/context.xml b/src/main/resources/tomcat/context.xml new file mode 100644 index 000000000000..9311d5904aea --- /dev/null +++ b/src/main/resources/tomcat/context.xml @@ -0,0 +1,57 @@ + + + + + + + + WEB-INF/web.xml + ${catalina.base}/conf/web.xml + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception.jsp new file mode 100644 index 000000000000..90f84f4b1aeb --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/exception.jsp @@ -0,0 +1,26 @@ +<%@ page isErrorPage="true" contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + +
+
+
+

${status}

+

+

${message}

+
+
+ + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp new file mode 100644 index 000000000000..62f05f9a4ac3 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -0,0 +1,30 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp new file mode 100644 index 000000000000..cf1331fd5523 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp @@ -0,0 +1,8 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%--https://getbootstrap.com/docs/4.0/examples/sticky-footer/--%> +
+
+ +
+
\ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp new file mode 100644 index 000000000000..6a442be51c19 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -0,0 +1,29 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> + + + + + + + + <spring:message code="app.title"/> + + + + + + + + + + + <%--http://stackoverflow.com/a/24070373/548473--%> + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp new file mode 100644 index 000000000000..3e2ad09be240 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp @@ -0,0 +1,14 @@ +<%@ page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/login.jsp b/src/main/webapp/WEB-INF/jsp/login.jsp new file mode 100644 index 000000000000..45bd22b72ad9 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/login.jsp @@ -0,0 +1,73 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> + + + + + + +
+
+ +
${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
+
+ +
+
+ +
+ » + + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp new file mode 100644 index 000000000000..607458923fec --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -0,0 +1,117 @@ +<%@ 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" %> +<%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %> + + + + + + + +
+
+

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

${userTo.name}

+ + + + + + + +
+ + + + + +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp new file mode 100644 index 000000000000..9d9b928265f4 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -0,0 +1,84 @@ +<%@ 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" %> + + + + + + + + +
+
+

+ + + + + + + + + + + + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/tags/inputField.tag b/src/main/webapp/WEB-INF/tags/inputField.tag new file mode 100644 index 000000000000..2d8f465488a1 --- /dev/null +++ b/src/main/webapp/WEB-INF/tags/inputField.tag @@ -0,0 +1,19 @@ +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +<%@ attribute name="name" required="true" description="Name of corresponding property in bean object" %> +<%@ attribute name="labelCode" required="true" description="Field label" %> +<%@ attribute name="inputType" required="false" description="Input type" %> + + +
+ + + + + + +
${status.errorMessage}
+
+
\ No newline at end of file diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld new file mode 100644 index 000000000000..d138fecdbfb5 --- /dev/null +++ b/src/main/webapp/WEB-INF/tld/functions.tld @@ -0,0 +1,16 @@ + + + + 1.0 + functions + http://topjava.javawebinar.ru/functions + + + formatDateTime + ru.javawebinar.topjava.util.DateTimeUtil + java.lang.String toString(java.time.LocalDateTime) + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..9816e2a1bd18 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,66 @@ + + + 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 + / + + + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + springSecurityFilterChain + /* + + + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + + diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css new file mode 100644 index 000000000000..e29b59dd23b9 --- /dev/null +++ b/src/main/webapp/resources/css/style.css @@ -0,0 +1,50 @@ +tr[data-mealExcess="false"] { + color: green; +} + +tr[data-mealExcess="true"] { + color: red; +} + +.fa { + cursor: pointer; +} + +tr[data-userEnabled="false"] { + opacity: 0.3; +} + +.error, .message { + padding: 10px; + border-radius: 4px; + font-size: 16px; +} + +.error { + color: #a94442; + background-color: #f2dede; + border: 1px solid #ebccd1; +} + +.message { + color: #2f9635; + background-color: #c6fbc2; + border: 1px solid #9feba6; +} + +/*https://getbootstrap.com/docs/4.0/examples/sticky-footer/sticky-footer.css*/ +html { + position: relative; + min-height: 100%; +} +body { + margin-bottom: 60px; /* Margin bottom by footer height */ +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + height: 60px; /* Set the fixed height of the footer here */ + line-height: 60px; /* Vertically center the text there */ + background-color: #f5f5f5; +} diff --git a/src/main/webapp/resources/images/icon-meal.png b/src/main/webapp/resources/images/icon-meal.png new file mode 100644 index 000000000000..b4fc54ad0129 Binary files /dev/null and b/src/main/webapp/resources/images/icon-meal.png differ diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js new file mode 100644 index 000000000000..30e47e8919f7 --- /dev/null +++ b/src/main/webapp/resources/js/topjava.common.js @@ -0,0 +1,116 @@ +var ajaxUrl, datatableApi, updateTable, form; + +function makeEditable(aUrl, datatableOpts, upTable) { + ajaxUrl = aUrl; + datatableApi = $("#datatable").DataTable( + // https://api.jquery.com/jquery.extend/#jQuery-extend-deep-target-object1-objectN + $.extend(true, datatableOpts, + { + "ajax": { + "url": ajaxUrl, + "dataSrc": "" + }, + "paging": false, + "info": true + } + )); + updateTable = upTable; + form = $('#detailsForm'); + $(document).ajaxError(function (event, jqXHR, options, jsExc) { + failNoty(jqXHR); + }); + + // solve problem with cache in IE: https://stackoverflow.com/a/4303862/548473 + $.ajaxSetup({cache: false}); + + var token = $("meta[name='_csrf']").attr("content"); + var header = $("meta[name='_csrf_header']").attr("content"); + $(document).ajaxSend(function (e, xhr, options) { + xhr.setRequestHeader(header, token); + }); +} + +function add() { + $("#modalTitle").html(i18n["addTitle"]); + form.find(":input").val(""); + $("#editRow").modal(); +} + +function updateRow(id) { + $("#modalTitle").html(i18n["editTitle"]); + $.get(ajaxUrl + id, function (data) { + $.each(data, function (key, value) { + form.find("input[name='" + key + "']").val(value); + }); + $('#editRow').modal(); + }); +} + +function deleteRow(id) { + if (confirm(i18n['common.confirm'])) { + $.ajax({ + url: ajaxUrl + id, + type: "DELETE" + }).done(function () { + updateTable(); + successNoty("common.deleted"); + }); + } +} + +function updateTableByData(data) { + datatableApi.clear().rows.add(data).draw(); +} + +function save() { + $.ajax({ + type: "POST", + url: ajaxUrl, + data: form.serialize() + }).done(function () { + $("#editRow").modal("hide"); + updateTable(); + successNoty("common.saved"); + }); +} + +var failedNote; + +function closeNoty() { + if (failedNote) { + failedNote.close(); + failedNote = undefined; + } +} + +function successNoty(key) { + closeNoty(); + new Noty({ + text: "  " + i18n[key], + type: 'success', + layout: "bottomRight", + timeout: 1000 + }).show(); +} + +function failNoty(jqXHR) { + closeNoty(); + var errorInfo = JSON.parse(jqXHR.responseText); + failedNote = new Noty({ + text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + "
" + errorInfo.type + "
" + errorInfo.detail, + type: "error", + layout: "bottomRight" + }).show(); +} + +function renderEditBtn(data, type, row) { + if (type === "display") { + return ""; + } +} + +function renderDeleteBtn(data, type, row) { + if (type === "display") { + return ""; + } +} \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.meals.js b/src/main/webapp/resources/js/topjava.meals.js new file mode 100644 index 000000000000..93d8e5ed4640 --- /dev/null +++ b/src/main/webapp/resources/js/topjava.meals.js @@ -0,0 +1,111 @@ +var mealAjaxUrl = "profile/meals/"; + +function updateFilteredTable() { + $.ajax({ + type: "GET", + url: mealAjaxUrl + "filter", + data: $("#filter").serialize() + }).done(updateTableByData); +} + +function clearFilter() { + $("#filter")[0].reset(); + $.get(mealAjaxUrl, updateTableByData); +} + +// http://api.jquery.com/jQuery.ajax/#using-converters +$.ajaxSetup({ + converters: { + "text json": function (stringData) { + var json = JSON.parse(stringData); + $(json).each(function () { + this.dateTime = this.dateTime.replace('T', ' ').substr(0, 16); + }); + return json; + } + } +}); + +$(function () { + makeEditable(mealAjaxUrl, { + "columns": [ + { + "data": "dateTime" + }, + { + "data": "description" + }, + { + "data": "calories" + }, + { + "render": renderEditBtn, + "defaultContent": "", + "orderable": false + }, + { + "render": renderDeleteBtn, + "defaultContent": "", + "orderable": false + } + ], + "order": [ + [ + 0, + "desc" + ] + ], + "createdRow": function (row, data, dataIndex) { + $(row).attr("data-mealExcess", data.excess); + }, + }, updateFilteredTable); + +// http://xdsoft.net/jqplugins/datetimepicker/ + var startDate = $('#startDate'); + var endDate = $('#endDate'); + startDate.datetimepicker({ + timepicker: false, + format: 'Y-m-d', + formatDate: 'Y-m-d', + onShow: function (ct) { + this.setOptions({ + maxDate: endDate.val() ? endDate.val() : false + }) + } + }); + endDate.datetimepicker({ + timepicker: false, + format: 'Y-m-d', + formatDate: 'Y-m-d', + onShow: function (ct) { + this.setOptions({ + minDate: startDate.val() ? startDate.val() : false + }) + } + }); + + var startTime = $('#startTime'); + var endTime = $('#endTime'); + startTime.datetimepicker({ + datepicker: false, + format: 'H:i', + onShow: function (ct) { + this.setOptions({ + maxTime: endTime.val() ? endTime.val() : false + }) + } + }); + endTime.datetimepicker({ + datepicker: false, + format: 'H:i', + onShow: function (ct) { + this.setOptions({ + minTime: startTime.val() ? startTime.val() : false + }) + } + }); + + $('#dateTime').datetimepicker({ + format: 'Y-m-d H:i' + }); +}); \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js new file mode 100644 index 000000000000..221858481c8e --- /dev/null +++ b/src/main/webapp/resources/js/topjava.users.js @@ -0,0 +1,80 @@ +var userAjaxUrl = "admin/users/"; + +function enable(chkbox, id) { + var enabled = chkbox.is(":checked"); +// https://stackoverflow.com/a/22213543/548473 + $.ajax({ + url: userAjaxUrl + id, + type: "POST", + data: "enabled=" + enabled + }).done(function () { + chkbox.closest("tr").attr("data-userEnabled", enabled); + successNoty(enabled ? "common.enabled" : "common.disabled"); + }).fail(function () { + $(chkbox).prop("checked", !enabled); + }); +} + +// $(document).ready(function () { +$(function () { + makeEditable(userAjaxUrl, { + "columns": [ + { + "data": "name" + }, + { + "data": "email", + "render": function (data, type, row) { + if (type === "display") { + return "" + data + ""; + } + return data; + } + }, + { + "data": "roles" + }, + { + "data": "enabled", + "render": function (data, type, row) { + if (type === "display") { + return ""; + } + return data; + } + }, + { + "data": "registered", + "render": function (date, type, row) { + if (type === "display") { + return date.substring(0, 10); + } + return date; + } + }, + { + "orderable": false, + "defaultContent": "", + "render": renderEditBtn + }, + { + "orderable": false, + "defaultContent": "", + "render": renderDeleteBtn + } + ], + "order": [ + [ + 0, + "asc" + ] + ], + "createdRow": function (row, data, dataIndex) { + if (!data.enabled) { + $(row).attr("data-userEnabled", false); + } + } + }, function () { + $.get(userAjaxUrl, updateTableByData); + }); +}); \ No newline at end of file diff --git a/src/main/webapp/test.html b/src/main/webapp/test.html new file mode 100644 index 000000000000..e50b3327754f --- /dev/null +++ b/src/main/webapp/test.html @@ -0,0 +1,13 @@ + + + + + +
+ +
+ + \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java new file mode 100644 index 000000000000..05d47b19f214 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java @@ -0,0 +1,12 @@ +package ru.javawebinar.topjava; + +import org.springframework.test.context.ActiveProfilesResolver; + +//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver +public class ActiveDbProfileResolver implements ActiveProfilesResolver { + + @Override + public String[] resolve(Class aClass) { + return new String[]{Profiles.getActiveDbProfile()}; + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java b/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java new file mode 100644 index 000000000000..4d1d1e9e3789 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/AllActiveProfileResolver.java @@ -0,0 +1,12 @@ +package ru.javawebinar.topjava; + +import org.springframework.test.context.ActiveProfilesResolver; + +//http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver +public class AllActiveProfileResolver implements ActiveProfilesResolver { + + @Override + public String[] resolve(Class aClass) { + return new String[]{Profiles.REPOSITORY_IMPLEMENTATION, Profiles.getActiveDbProfile()}; + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java new file mode 100644 index 000000000000..637a49df8f24 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; + +import java.time.Month; +import java.util.List; + +import static java.time.LocalDateTime.of; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class MealTestData { + public static TestMatcher MEAL_MATCHER = TestMatcher.usingFieldsWithIgnoringAssertions(Meal.class, "user"); + public static TestMatcher MEAL_TO_MATCHER = TestMatcher.usingEqualsAssertions(MealTo.class); + + 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 ADMIN_MEAL1 = new Meal(ADMIN_MEAL_ID, of(2020, Month.JANUARY, 31, 14, 0), "Админ ланч", 510); + public static final Meal ADMIN_MEAL2 = 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(), "Обновленный завтрак", 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..302739536c03 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava; + +import org.springframework.context.support.GenericXmlApplicationContext; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.web.meal.MealRestController; +import ru.javawebinar.topjava.web.user.AdminRestController; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +import static ru.javawebinar.topjava.TestUtil.mockAuthorize; +import static ru.javawebinar.topjava.UserTestData.USER; + +public class SpringMain { + public static void main(String[] args) { + // java 7 automatic resource management + try (GenericXmlApplicationContext appCtx = new GenericXmlApplicationContext()) { + appCtx.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION); + appCtx.load("spring/inmemory.xml"); + appCtx.refresh(); + + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); + AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); + adminUserController.create(new User(null, "userName", "email@mail.ru", "password", 2000, Role.ADMIN)); + System.out.println(); + + mockAuthorize(USER); + + 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..243e64599b78 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TestMatcher.java @@ -0,0 +1,63 @@ +package ru.javawebinar.topjava; + +import org.springframework.test.web.servlet.ResultMatcher; + +import java.util.List; +import java.util.function.BiConsumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.TestUtil.readListFromJsonMvcResult; + +public class TestMatcher { + private final Class clazz; + private final BiConsumer assertion; + private final BiConsumer, Iterable> iterableAssertion; + + private TestMatcher(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) { + this.clazz = clazz; + this.assertion = assertion; + this.iterableAssertion = iterableAssertion; + } + + public static TestMatcher usingAssertions(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) { + return new TestMatcher<>(clazz, assertion, iterableAssertion); + } + + public static TestMatcher usingEqualsAssertions(Class clazz) { + return usingAssertions(clazz, + (a, e) -> assertThat(a).isEqualTo(e), + (a, e) -> assertThat(a).isEqualTo(e)); + } + + public static TestMatcher usingFieldsWithIgnoringAssertions(Class clazz, String... fieldsToIgnore) { + return usingAssertions(clazz, + (a, e) -> assertThat(a).isEqualToIgnoringGivenFields(e, fieldsToIgnore), + (a, e) -> assertThat(a).usingElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(e)); + } + + public void assertMatch(T actual, T expected) { + assertion.accept(actual, expected); + } + + @SafeVarargs + public final void assertMatch(Iterable actual, T... expected) { + assertMatch(actual, List.of(expected)); + } + + public void assertMatch(Iterable actual, Iterable expected) { + iterableAssertion.accept(actual, expected); + } + + public ResultMatcher contentJson(T expected) { + return result -> assertMatch(TestUtil.readFromJsonMvcResult(result, clazz), expected); + } + + @SafeVarargs + public final ResultMatcher contentJson(T... expected) { + return contentJson(List.of(expected)); + } + + public ResultMatcher contentJson(Iterable expected) { + return result -> assertMatch(readListFromJsonMvcResult(result, clazz), expected); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/TestUtil.java b/src/test/java/ru/javawebinar/topjava/TestUtil.java new file mode 100644 index 000000000000..8103c5b08cae --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.io.UnsupportedEncodingException; +import java.util.List; + +public class TestUtil { + public static String getContent(MvcResult result) throws UnsupportedEncodingException { + return result.getResponse().getContentAsString(); + } + + public static T readFromJson(ResultActions action, Class clazz) throws UnsupportedEncodingException { + return JsonUtil.readValue(getContent(action.andReturn()), clazz); + } + + public static T readFromJsonMvcResult(MvcResult result, Class clazz) throws UnsupportedEncodingException { + return JsonUtil.readValue(getContent(result), clazz); + } + + public static List readListFromJsonMvcResult(MvcResult result, Class clazz) throws UnsupportedEncodingException { + return JsonUtil.readValues(getContent(result), clazz); + } + + public static void mockAuthorize(User user) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(new AuthorizedUser(user), null, user.getRoles())); + } + + public static RequestPostProcessor userHttpBasic(User user) { + return SecurityMockMvcRequestPostProcessors.httpBasic(user.getEmail(), user.getPassword()); + } + + public static RequestPostProcessor userAuth(User user) { + return SecurityMockMvcRequestPostProcessors.authentication(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword())); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/TimingExtension.java b/src/test/java/ru/javawebinar/topjava/TimingExtension.java new file mode 100644 index 000000000000..d2d75becac14 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TimingExtension.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava; + +import org.junit.jupiter.api.extension.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StopWatch; + +public class TimingExtension implements + BeforeTestExecutionCallback, AfterTestExecutionCallback, BeforeAllCallback, AfterAllCallback { + + private static final Logger log = LoggerFactory.getLogger("result"); + + private StopWatch stopWatch; + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + stopWatch = new StopWatch("Execution time of " + extensionContext.getRequiredTestClass().getSimpleName()); + } + + @Override + public void beforeTestExecution(ExtensionContext extensionContext) throws Exception { + String testName = extensionContext.getDisplayName(); + log.info("\nStart " + testName); + stopWatch.start(testName); + } + + @Override + public void afterTestExecution(ExtensionContext extensionContext) throws Exception { + stopWatch.stop(); + } + + @Override + public void afterAll(ExtensionContext extensionContext) throws Exception { + log.info('\n' + stopWatch.prettyPrint() + '\n'); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java new file mode 100644 index 000000000000..d5c2d1a56c0c --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -0,0 +1,52 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class UserTestData { + public static TestMatcher USER_MATCHER = TestMatcher.usingFieldsWithIgnoringAssertions(User.class, "registered", "meals", "password"); + public static TestMatcher USER_WITH_MEALS_MATCHER = + TestMatcher.usingAssertions(User.class, + (a, e) -> assertThat(a).usingRecursiveComparison() + .ignoringFields("registered", "meals.user", "password").ignoringAllOverriddenEquals().isEqualTo(e), + (a, e) -> { + throw new UnsupportedOperationException(); + }); + + public static final int NOT_FOUND = 10; + public static final int USER_ID = START_SEQ; + public static final int ADMIN_ID = START_SEQ + 1; + + public static final User USER = new User(USER_ID, "User", "user@yandex.ru", "password", 2005, Role.USER); + public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", 1900, Role.ADMIN, Role.USER); + + static { + USER.setMeals(MEALS); + ADMIN.setMeals(List.of(ADMIN_MEAL2, ADMIN_MEAL1)); + } + + 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.setName("UpdatedName"); + updated.setCaloriesPerDay(330); + updated.setRoles(Collections.singletonList(Role.ADMIN)); + return updated; + } + + public static String jsonWithPassword(User user, String passw) { + return JsonUtil.writeAdditionProps(user, "password", passw); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java new file mode 100644 index 000000000000..317bbcabbd1e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryBaseRepository.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.repository.inmemory; + +import ru.javawebinar.topjava.model.AbstractBaseEntity; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; + +public class InMemoryBaseRepository { + + private static final AtomicInteger counter = new AtomicInteger(START_SEQ); + + final Map map = new ConcurrentHashMap<>(); + + public T save(T entry) { + if (entry.isNew()) { + entry.setId(counter.incrementAndGet()); + map.put(entry.getId(), entry); + return entry; + } + return map.computeIfPresent(entry.getId(), (id, oldT) -> entry); + } + + public boolean delete(int id) { + return map.remove(id) != null; + } + + public T get(int id) { + return map.get(id); + } + + Collection getCollection() { + return map.values(); + } +} \ No newline at end of file diff --git a/src/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..fad599d4dd86 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryMealRepository.java @@ -0,0 +1,83 @@ +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.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +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(meal -> userMeals.map.put(meal.getId(), meal)); + usersMealsMap.put(UserTestData.USER_ID, userMeals); + } + + + @Override + public Meal save(Meal meal, int userId) { + 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..d08419ab6aa3 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/inmemory/InMemoryUserRepository.java @@ -0,0 +1,39 @@ +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.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(); + map.put(UserTestData.USER_ID, USER); + map.put(UserTestData.ADMIN_ID, ADMIN); + } + + @Override + public List getAll() { + return getCollection().stream() + .sorted(Comparator.comparing(User::getName).thenComparing(User::getEmail)) + .collect(Collectors.toList()); + } + + @Override + public User getByEmail(String email) { + 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..768b3cefdebd --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractMealServiceTest.java @@ -0,0 +1,104 @@ +package ru.javawebinar.topjava.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import javax.validation.ConstraintViolationException; +import java.time.LocalDate; +import java.time.Month; + +import static java.time.LocalDateTime.of; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +public abstract class AbstractMealServiceTest extends AbstractServiceTest { + + @Autowired + protected MealService service; + + @Test + void delete() throws Exception { + service.delete(MEAL1_ID, USER_ID); + assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, USER_ID)); + } + + @Test + void deleteNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND, USER_ID)); + } + + @Test + void deleteNotOwn() throws Exception { + assertThrows(NotFoundException.class, () -> service.delete(MEAL1_ID, ADMIN_ID)); + } + + @Test + void create() throws Exception { + 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 + void get() throws Exception { + Meal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); + MEAL_MATCHER.assertMatch(actual, ADMIN_MEAL1); + } + + @Test + void getNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND, USER_ID)); + } + + @Test + void getNotOwn() throws Exception { + assertThrows(NotFoundException.class, () -> service.get(MEAL1_ID, ADMIN_ID)); + } + + @Test + void update() throws Exception { + Meal updated = getUpdated(); + service.update(updated, USER_ID); + MEAL_MATCHER.assertMatch(service.get(MEAL1_ID, USER_ID), getUpdated()); + } + + @Test + void updateNotOwn() throws Exception { + NotFoundException exception = assertThrows(NotFoundException.class, () -> service.update(MEAL1, ADMIN_ID)); + Assertions.assertEquals("Not found entity with id=" + MEAL1_ID, exception.getMessage()); + } + + @Test + void getAll() throws Exception { + MEAL_MATCHER.assertMatch(service.getAll(USER_ID), MEALS); + } + + @Test + void getBetweenInclusive() throws Exception { + MEAL_MATCHER.assertMatch(service.getBetweenInclusive( + LocalDate.of(2020, Month.JANUARY, 30), + LocalDate.of(2020, Month.JANUARY, 30), USER_ID), + MEAL3, MEAL2, MEAL1); + } + + @Test + void getBetweenWithNullDates() throws Exception { + MEAL_MATCHER.assertMatch(service.getBetweenInclusive(null, null, USER_ID), MEALS); + } + + @Test + void createWithException() throws Exception { + validateRootCause(() -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), " ", 300), USER_ID), ConstraintViolationException.class); + validateRootCause(() -> service.create(new Meal(null, null, "Description", 300), USER_ID), ConstraintViolationException.class); + validateRootCause(() -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 9), USER_ID), ConstraintViolationException.class); + validateRootCause(() -> service.create(new Meal(null, of(2015, Month.JUNE, 1, 18, 0), "Description", 5001), USER_ID), ConstraintViolationException.class); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java new file mode 100644 index 000000000000..9b42af1f21cb --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -0,0 +1,34 @@ +package ru.javawebinar.topjava.service; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import ru.javawebinar.topjava.ActiveDbProfileResolver; +import ru.javawebinar.topjava.TimingExtension; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause; + +@SpringJUnitConfig(locations = { + "classpath:spring/spring-app.xml", + "classpath:spring/spring-db.xml" +}) +//@ExtendWith(SpringExtension.class) +@ActiveProfiles(resolver = ActiveDbProfileResolver.class) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@ExtendWith(TimingExtension.class) +abstract public class AbstractServiceTest { + + // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 + public void validateRootCause(Runnable runnable, Class rootExceptionClass) { + 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..5fc6bec55979 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -0,0 +1,96 @@ +package ru.javawebinar.topjava.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import javax.validation.ConstraintViolationException; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static ru.javawebinar.topjava.UserTestData.*; + +public abstract class AbstractUserServiceTest extends AbstractServiceTest { + + @Autowired + protected UserService service; + + @Test + void create() throws Exception { + 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 + void duplicateMailCreate() throws Exception { + assertThrows(DataAccessException.class, () -> + service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", 2000, Role.USER))); + } + + @Test + void delete() throws Exception { + service.delete(USER_ID); + assertThrows(NotFoundException.class, () -> service.get(USER_ID)); + } + + @Test + void deletedNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> service.delete(NOT_FOUND)); + } + + @Test + void get() throws Exception { + User user = service.get(ADMIN_ID); + USER_MATCHER.assertMatch(user, ADMIN); + } + + @Test + void getNotFound() throws Exception { + assertThrows(NotFoundException.class, () -> service.get(NOT_FOUND)); + } + + @Test + void getByEmail() throws Exception { + User user = service.getByEmail("admin@gmail.com"); + USER_MATCHER.assertMatch(user, ADMIN); + } + + @Test + void update() throws Exception { + User updated = getUpdated(); + service.update(updated); + USER_MATCHER.assertMatch(service.get(USER_ID), getUpdated()); + } + + @Test + void getAll() throws Exception { + List all = service.getAll(); + USER_MATCHER.assertMatch(all, ADMIN, USER); + } + + @Test + void createWithException() throws Exception { + validateRootCause(() -> service.create(new User(null, " ", "mail@yandex.ru", "password", 2000, Role.USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", " ", "password", 2000, Role.USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", " ", 2000, Role.USER)), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Set.of())), ConstraintViolationException.class); + validateRootCause(() -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Set.of())), ConstraintViolationException.class); + } + + @Test + void enable() { + service.enable(USER_ID, false); + assertFalse(service.get(USER_ID).isEnabled()); + service.enable(USER_ID, true); + assertTrue(service.get(USER_ID).isEnabled()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java new file mode 100644 index 000000000000..2d66bd0ec3c3 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaMealServiceTest.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; + +@ActiveProfiles(DATAJPA) +class DataJpaMealServiceTest extends AbstractMealServiceTest { + @Test + void getWithUser() throws Exception { + Meal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID); + MEAL_MATCHER.assertMatch(adminMeal, ADMIN_MEAL1); + UserTestData.USER_MATCHER.assertMatch(adminMeal.getUser(), UserTestData.ADMIN); + } + + @Test + void getWithUserNotFound() throws Exception { + Assertions.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..feb99f59b920 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -0,0 +1,26 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.AbstractUserServiceTest; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.*; + +@ActiveProfiles(DATAJPA) +class DataJpaUserServiceTest extends AbstractUserServiceTest { + @Test + void getWithMeals() throws Exception { + User admin = service.getWithMeals(ADMIN_ID); + USER_WITH_MEALS_MATCHER.assertMatch(admin, ADMIN); + } + + @Test + void getWithMealsNotFound() throws Exception { + Assertions.assertThrows(NotFoundException.class, + () -> service.getWithMeals(1)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java new file mode 100644 index 000000000000..aef588264f71 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles(JDBC) +class JdbcMealServiceTest extends AbstractMealServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java new file mode 100644 index 000000000000..62ca7668cf67 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles(JDBC) +class JdbcUserServiceTest extends AbstractUserServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java new file mode 100644 index 000000000000..aaf5dcda960e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles(JPA) +class JpaMealServiceTest extends AbstractMealServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java new file mode 100644 index 000000000000..6d1cd91543fc --- /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) +class JpaUserServiceTest extends AbstractUserServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java new file mode 100644 index 000000000000..feba0b02f372 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -0,0 +1,64 @@ +package ru.javawebinar.topjava.web; + +import org.junit.jupiter.api.Assumptions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import ru.javawebinar.topjava.AllActiveProfileResolver; +import ru.javawebinar.topjava.Profiles; + +import javax.annotation.PostConstruct; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + +@SpringJUnitWebConfig(locations = { + "classpath:spring/spring-app.xml", + "classpath:spring/spring-mvc.xml", + "classpath:spring/spring-db.xml" +}) +//@WebAppConfiguration +//@ExtendWith(SpringExtension.class) +@Transactional +@ActiveProfiles(resolver = AllActiveProfileResolver.class) +abstract public class AbstractControllerTest { + + private static final CharacterEncodingFilter CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter(); + + @Autowired + public Environment env; + + static { + CHARACTER_ENCODING_FILTER.setEncoding("UTF-8"); + CHARACTER_ENCODING_FILTER.setForceEncoding(true); + } + + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext webApplicationContext; + + public void assumeDataJpa() { + Assumptions.assumeTrue(env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.DATAJPA)), "DATA-JPA only"); + } + + @PostConstruct + private void postConstruct() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .addFilter(CHARACTER_ENCODING_FILTER) + .apply(springSecurity()) + .build(); + } + + public ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception { + return mockMvc.perform(builder); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java new file mode 100644 index 000000000000..550ed9cef0b5 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.web; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ResourceControllerTest extends AbstractControllerTest { + + @Test + void resources() throws Exception { + perform(get("/resources/css/style.css")) + .andDo(print()) + .andExpect(content().contentType(MediaType.valueOf("text/css"))) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java new file mode 100644 index 000000000000..0f2fe659d637 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -0,0 +1,41 @@ +package ru.javawebinar.topjava.web; + +import org.junit.jupiter.api.Test; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static ru.javawebinar.topjava.TestUtil.userAuth; +import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER; + +class RootControllerTest extends AbstractControllerTest { + + @Test + void getUsers() throws Exception { + perform(get("/users") + .with(userAuth(ADMIN))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("users")) + .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")); + } + + @Test + void unAuth() throws Exception { + perform(get("/users")) + .andDo(print()) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + void getMeals() throws Exception { + perform(get("/meals") + .with(userAuth(USER))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("meals")) + .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp")); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java new file mode 100644 index 000000000000..af301c1c7178 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava.web.json; + +import org.junit.jupiter.api.Test; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static ru.javawebinar.topjava.MealTestData.*; + +class JsonUtilTest { + + @Test + void readWriteValue() throws Exception { + String json = JsonUtil.writeValue(ADMIN_MEAL1); + System.out.println(json); + Meal meal = JsonUtil.readValue(json, Meal.class); + MEAL_MATCHER.assertMatch(meal, ADMIN_MEAL1); + } + + @Test + void readWriteValues() throws Exception { + String json = JsonUtil.writeValue(MEALS); + System.out.println(json); + List meals = JsonUtil.readValues(json, Meal.class); + MEAL_MATCHER.assertMatch(meals, MEALS); + } + + @Test + void writeOnlyAccess() throws Exception { + String json = JsonUtil.writeValue(UserTestData.USER); + System.out.println(json); + assertThat(json, not(containsString("password"))); + String jsonWithPass = UserTestData.jsonWithPassword(UserTestData.USER, "newPass"); + System.out.println(jsonWithPass); + User user = JsonUtil.readValue(jsonWithPass, User.class); + assertEquals(user.getPassword(), "newPass"); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java new file mode 100644 index 000000000000..3001f6a8f2cb --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -0,0 +1,128 @@ +package ru.javawebinar.topjava.web.meal; + + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.TestUtil.readFromJson; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.USER; +import static ru.javawebinar.topjava.UserTestData.USER_ID; +import static ru.javawebinar.topjava.util.MealsUtil.createTo; +import static ru.javawebinar.topjava.util.MealsUtil.getTos; + +class MealRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = MealRestController.REST_URL + '/'; + + @Autowired + private MealService mealService; + + @Test + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + MEAL1_ID) + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(MEAL_MATCHER.contentJson(MEAL1)); + } + + @Test + void getUnauth() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + MEAL1_ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + void getNotFound() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void delete() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + MEAL1_ID) + .with(userHttpBasic(USER))) + .andExpect(status().isNoContent()); + assertThrows(NotFoundException.class, () -> mealService.get(MEAL1_ID, USER_ID)); + } + + @Test + void deleteNotFound() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(USER))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void update() throws Exception { + Meal updated = MealTestData.getUpdated(); + perform(MockMvcRequestBuilders.put(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated)) + .with(userHttpBasic(USER))) + .andExpect(status().isNoContent()); + + MEAL_MATCHER.assertMatch(mealService.get(MEAL1_ID, USER_ID), updated); + } + + @Test + void createWithLocation() throws Exception { + Meal newMeal = MealTestData.getNew(); + ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(newMeal)) + .with(userHttpBasic(USER))); + + Meal created = readFromJson(action, Meal.class); + int newId = created.id(); + newMeal.setId(newId); + MEAL_MATCHER.assertMatch(created, newMeal); + MEAL_MATCHER.assertMatch(mealService.get(newId, USER_ID), newMeal); + } + + @Test + void getAll() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(MEAL_TO_MATCHER.contentJson(getTos(MEALS, USER.getCaloriesPerDay()))); + } + + @Test + void filter() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + "filter") + .param("startDate", "2020-01-30").param("startTime", "07:00") + .param("endDate", "2020-01-31").param("endTime", "11:00") + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(MEAL_TO_MATCHER.contentJson(createTo(MEAL5, true), createTo(MEAL1, false))); + } + + @Test + void filterAll() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + "filter?startDate=&endTime=") + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andExpect(MEAL_TO_MATCHER.contentJson(getTos(MEALS, USER.getCaloriesPerDay()))); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java new file mode 100644 index 000000000000..070ddb494266 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -0,0 +1,149 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.TestUtil.readFromJson; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.*; + +class AdminRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = AdminRestController.REST_URL + '/'; + + @Autowired + private UserService userService; + + @Test + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andDo(print()) + // https://jira.spring.io/browse/SPR-14472 + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_MATCHER.contentJson(ADMIN)); + } + + @Test + void getNotFound() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + 1) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andDo(print()); + } + + @Test + void getByEmail() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + "by?email=" + ADMIN.getEmail()) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_MATCHER.contentJson(ADMIN)); + } + + @Test + void delete() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + USER_ID) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isNoContent()); + assertThrows(NotFoundException.class, () -> userService.get(USER_ID)); + } + + @Test + void deleteNotFound() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + 1) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isUnprocessableEntity()) + .andDo(print()); + } + + @Test + void getUnAuth() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + void getForbidden() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isForbidden()); + } + + @Test + void update() throws Exception { + User updated = getUpdated(); + perform(MockMvcRequestBuilders.put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(JsonUtil.writeValue(updated))) + .andExpect(status().isNoContent()); + + USER_MATCHER.assertMatch(userService.get(USER_ID), updated); + } + + @Test + void createWithLocation() throws Exception { + User newUser = getNew(); + ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(UserTestData.jsonWithPassword(newUser, "newPass"))) + .andExpect(status().isCreated()); + + User created = readFromJson(action, User.class); + int newId = created.id(); + newUser.setId(newId); + USER_MATCHER.assertMatch(created, newUser); + USER_MATCHER.assertMatch(userService.get(newId), newUser); + } + + @Test + void getAll() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_MATCHER.contentJson(ADMIN, USER)); + } + + @Test + void enable() throws Exception { + perform(MockMvcRequestBuilders.patch(REST_URL + USER_ID) + .param("enabled", "false") + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isNoContent()); + + assertFalse(userService.get(USER_ID).isEnabled()); + } + + @Test + void getWithMeals() throws Exception { + assumeDataJpa(); + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID + "/with-meals") + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_WITH_MEALS_MATCHER.contentJson(ADMIN)); + } +} \ 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..21bfd53b15fe --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import ru.javawebinar.topjava.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; + +@SpringJUnitConfig(locations = {"classpath:spring/inmemory.xml"}) +class InMemoryAdminRestControllerSpringTest { + + @Autowired + private AdminRestController controller; + + @Autowired + private InMemoryUserRepository repository; + + @BeforeEach + void setup() throws Exception { + repository.init(); + } + + @Test + void delete() throws Exception { + controller.delete(USER_ID); + Assertions.assertNull(repository.get(USER_ID)); + } + + @Test + void deleteNotFound() throws Exception { + Assertions.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..3064da4993e0 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.*; +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; + +class InMemoryAdminRestControllerTest { + private static final Logger log = LoggerFactory.getLogger(InMemoryAdminRestControllerTest.class); + + private static ConfigurableApplicationContext appCtx; + private static AdminRestController controller; + private static InMemoryUserRepository repository; + + @BeforeAll + static void beforeClass() { + appCtx = new ClassPathXmlApplicationContext("spring/inmemory.xml"); + log.info("\n{}\n", Arrays.toString(appCtx.getBeanDefinitionNames())); + controller = appCtx.getBean(AdminRestController.class); + repository = appCtx.getBean(InMemoryUserRepository.class); + } + + @AfterAll + static void afterClass() { +// May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed +// http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite +// appCtx.close(); + } + + @BeforeEach + void setup() throws Exception { + // re-initialize + repository.init(); + } + + @Test + void delete() throws Exception { + controller.delete(USER_ID); + Assertions.assertNull(repository.get(USER_ID)); + } + + @Test + public void deleteNotFound() throws Exception { + Assertions.assertThrows(NotFoundException.class, () -> controller.delete(NOT_FOUND)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java new file mode 100644 index 000000000000..87033d03ac5c --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -0,0 +1,90 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.TestUtil.readFromJson; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; + +class ProfileRestControllerTest extends AbstractControllerTest { + + @Autowired + private UserService userService; + + @Test + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_MATCHER.contentJson(USER)); + } + + @Test + void getUnAuth() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + void delete() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isNoContent()); + USER_MATCHER.assertMatch(userService.getAll(), ADMIN); + } + + @Test + void register() throws Exception { + UserTo newTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500); + User newUser = UserUtil.createNewFromTo(newTo); + ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL + "/register") + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(newTo))) + .andDo(print()) + .andExpect(status().isCreated()); + + User created = readFromJson(action, User.class); + int newId = created.getId(); + newUser.setId(newId); + USER_MATCHER.assertMatch(created, newUser); + USER_MATCHER.assertMatch(userService.get(newId), newUser); + } + + @Test + void update() throws Exception { + UserTo updatedTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500); + perform(MockMvcRequestBuilders.put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER)) + .content(JsonUtil.writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isNoContent()); + + USER_MATCHER.assertMatch(userService.get(USER_ID), UserUtil.updateFromTo(new User(USER), updatedTo)); + } + + @Test + void getWithMeals() throws Exception { + assumeDataJpa(); + perform(MockMvcRequestBuilders.get(REST_URL + "/with-meals") + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_WITH_MEALS_MATCHER.contentJson(USER)); + } +} \ 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..f7e2dbbd4099 --- /dev/null +++ b/src/test/resources/spring/inmemory.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/src/test/resources/spring/spring-cache.xml b/src/test/resources/spring/spring-cache.xml new file mode 100644 index 000000000000..92ae4641b704 --- /dev/null +++ b/src/test/resources/spring/spring-cache.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + false + + + + + + \ No newline at end of file