From 273bb3586e2015780f1159bb3a31fdec89553859 Mon Sep 17 00:00:00 2001 From: JavaOPs Date: Thu, 7 Sep 2017 13:48:34 +0300 Subject: [PATCH 001/139] Add info --- README.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ ReleaseNotes.md | 127 +++++++++++++++++++++++++++++++++++ cv.md | 102 ++++++++++++++++++++++++++++ description.md | 76 +++++++++++++++++++++ 4 files changed, 479 insertions(+) create mode 100644 README.md create mode 100644 ReleaseNotes.md create mode 100644 cv.md create mode 100644 description.md diff --git a/README.md b/README.md new file mode 100644 index 000000000000..298d83554d1c --- /dev/null +++ b/README.md @@ -0,0 +1,174 @@ +Java Enterprise Online Project +=============================== +Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), datatables, jQuery + plugins, Java 8 Stream and Time API и хранением в базах данных Postgresql и HSQLDB. + +![topjava_structure](https://user-images.githubusercontent.com/13649199/27433714-8294e6fe-575e-11e7-9c41-7f6e16c5ebe5.jpg) + + Когда вы слышите что-то, вы забываете это. + Когда вы видите что-то, вы запоминаете это. + Но только когда вы начинаете делать это, + вы начинаете понимать это + + Старинная китайская поговорка + +## Описание и план проекта +### Демо разрабатываемого приложения +### [Изменения проекта (Release Notes)](ReleaseNotes.md) +### Требования к участникам, Wiki +### Составление резюме, подготовка к интервью, поиск работы + +Вводное занятие +=============== +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Осваиваем Java Enterprise. Трудоустройство. Ответы на вопросы. +- Слайды презентации +- Java Tools and Technologies Landscape Report 2016 +- [Java in 2017 Survey](http://www.baeldung.com/java-in-2017) +- Из юниоров в разработчики: получаем первую работу + +#### Spring Pet-Clinic +- Spring PetClinic Sample Application +- Demo +- Presentation + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Системы управления версиями. Git. +- **Wiki по ведению проекта в Git** +- Система управления версиями. VCS/DVSC. +- Ресурсы: + - Интерактивная Git обучалка + - Еще одна интерактивная обучалка, по русски + - Книга Git + - Working with remote repositories + - Видео по обучению Git + - Git Overview + - Видеокурс по Git + - [Основы Git за 20 минут](https://www.youtube.com/watch?v=TMeZGvtQnT8) + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Работа с проектом (выполнять инструкции) +- **Prepare_ to_ HW0.patch (скачать и положить в каталог вашего проекта)** + +## Инструкция по шагам (из видео): +- Установить ПО (git, JDK8, IntelliJ IDEA, Maven) +- Создать аккаунт на GitHub +- Сделать Fork ЭТОГО проекта (https://github.com/JavaOPs/topjava) +- Сделать локальный репозиторий проекта: +
git clone https://github.com/[Ваш аккаунт]/topjava.git
+- Открыть и настроить проект в IDEA + - Выставить кодировку UTF-8 в консоли + - Поставить кодировку UTF-8 + - Поменять фонт по умолчанию (DejaVu) +- По ходу видео сделать Apply Patch... скаченного патча Prepare_ to_ HW0.patch +- Закоммитить и запушить изменения (commit + push) +- Сделать ветку домашнего задания +- Выполнить задание и залить на GitHub (commit + push) +- Переключиться в основную ветку проекта master. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW0 +``` +Реализовать метод UserMealsUtil.getFilteredWithExceeded: +- должны возвращаться только записи между startTime и endTime +- поле UserMealWithExceed.exceed должно показывать, + превышает ли сумма калорий за весь день параметра метода caloriesPerDay + +Т.е UserMealWithExceed - это запись одной еды, но поле exceeded будет одинаково для всех записей за этот день. + +- Проверте результат выполнения ДЗ (можно проверить логику в http://topjava.herokuapp.com , список еды) +- Оцените Time complexity вашего алгоритма, если он O(N*N)- попробуйте сделать O(N). +``` +- Java 8 Date and Time API +- Алгоритмы и структуры данных для начинающих: сложность алгоритмов +- Time complexity +- Временная сложность алгоритма +- Вычислительная сложность + +#### Optional (Java 8 Stream API) +``` +Сделать реализация через Java 8 Stream API. Заменяйте forEach оператором stream.map(..) +``` +- Видео: Доступно о Java 8 Lambda +- Java 8: Lambda выражения +- Java 8: Потоки +- Pуководство по Java 8 Stream +- Лямбда-выражения в Java 8 +- A Guide to Java 8 +- Шпаргалка Java Stream API +- Алексея Владыкин: Элементы функционального программирования в Java +- Yakov Fain о новом в Java 8 +- stream.map vs forEach +- Дополнительно + - [Сергей Куксенко — Stream API, часть 1](https://www.youtube.com/watch?v=O8oN4KSZEXE) + - [Сергей Куксенко — Stream API, часть 2](https://www.youtube.com/watch?v=i0Jr2l3jrDA) + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания к HW0 +- Код проекта менять можно! Одна из распространенных ошибок как в тестовых заданиях на собеседовании, так и при работе на проекте, что ничего нельзя менять. Конечно при правках в рабочем проекте обязятельно нужно проконсультироваться/проревьюится у авторов кода (находится по истории VCS) +- Наследовать `UserMealWithExceed` от `UserMeal` я не буду, т.к. это разные сущности: Transfer Object и Entity. Мы будет их проходить на 2м уроке. +- Правильная реализация должна быть простой и красивой, можно сделать 2-мя способами: через стримы и через циклы. Сложность должна быть O(N), т.е. без вложенных стримов и циклов. +- При реализации через циклы посмотрите в `Map` на методы `getOrDefault` или `merge` +- При реализации через `Stream` заменяйте `forEach` оператором `stream.map(..)` +- Объявляйте переменные непосредственно перед использованием (если возможно - сразу с инициализацией). При объявлении коллекций используйте тип переменной - интерфейс (Map, List, ..) +- Если IDEA предлагает оптимизацию (желтым подчеркивает), нарпимер заменить лямбду на метод-референс, соглашайтесь (Alt+Enter) +- Пользуйтесь форматированием кода в IDEA: `Alt+Ctrl+L` +- Перед check-in проверяйте чендж-лист (курсор на файл и Ctrl+D): не оставляйте в коде ничего лишнего (закомментированный код, TODO и пр.). Если файл не меняется (например только пробелы или переводы строк), не надо его чекинить, делайте ему `revert` (Git -> Revert / `Ctrl+Alt+Z`). +- `System.out.println` нельзя делать нигде, кроме как в `main`. Позже введем логирование. +- Результаты, возвращаемые `UserMealsUtil.getFilteredWithExceeded` мы будем использовать [в нашем приложении](http://topjava.herokuapp.com/) для фильтрации по времени и отображения еды правильным цветом. +- Обращайте внимание на комментарии к вашим коммитам в git. Они должны быть короткие и информативные (лучше на english) + +----- + +### Полезные ресурсы +> ВНИМАНИЕ: основы JavaSсript необходимы для понимания проекта, начиная с 8-го занятия! + +Все остальное - опционально. + +#### HTML, JavaScript, CSS +- [Справочник по WEB](https://developer.mozilla.org/ru/) +- [Видео по WEB технологиям](https://www.youtube.com/user/WebMagistersRu/playlists) +- [Изучение JavaScript в одном видео уроке за час](https://www.youtube.com/watch?v=QBWWplFkdzw) +- HTML, CSS, JAVASCRIPT, SQL, JQUERY, BOOTSTRAP +- Введение в программирование на JavaScript +- Стандарты кодирования для HTML, CSS и JavaScript’a +- Основы работы с HTML/CSS/JavaScript +- JavaScript - Основы +- Основы JavaScript +- Bootstrap 3 - Основы +- jQuery для начинающих + +#### Java (базовые вещи) +- Интуит. Программирование на Java +- 1й урок MasterJava: Многопоточность +- Основы Java garbage collection +- Размер Java объектов +- Введение в Java Reflection API +- Структуры данных в картинках +- Обзор java.util.concurrent.* +- Синхронизация потоков +- String literal pool +- Маленькие хитрости Java +- A Guide to Java 8 + +### Туториалы, разное +[Что нужно знать о бэкенде новичку в веб-разработке](https://tproger.ru/translations/backend-web-development) +[Туториалы: Spring Framework, Hibernate, Java Core, JDBC](http://proselyte.net/tutorials/) + +#### Сервлеты +- Как создать Servlet? Полное руководство. +- Простое CRUD приложение на JSP и сервлетах + +#### JDBC, SQL +- Основы SQL на примере задачи +- Уроки по JDBC +- Learn SQL +- Интуит. Основы SQL +- Try SQL +- Курс "Введение в базы данных" + +#### Разное +- Эффективная работа с кодом в IntelliJ IDEA +- Quizful- тесты онлайн +- Введение в Linux + +#### Книги +- Джошуа Блох: Java. Эффективное программирование. Второе издание +- Гамма, Хелм, Джонсон: Приемы объектно-ориентированного проектирования. Паттерны проектирования +- Редмонд Э.: Семь баз данных за семь недель. Введение в современные базы данных и идеологию NoSQL +- Brian Goetz: Java Concurrency in Practice +- G.L. McDowell: Cracking the Coding Interview diff --git a/ReleaseNotes.md b/ReleaseNotes.md new file mode 100644 index 000000000000..cb2a81fbe347 --- /dev/null +++ b/ReleaseNotes.md @@ -0,0 +1,127 @@ +# TopJava Release Notes +### 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/usrer.jsp` в `mealForm.jsp/userFord.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). + +### Topjava 10 +- добавил + - доступ к AuthorizedUser через [`@AuthenticationPrincipal`](http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-authentication-principal) и [authentication Tag](http://docs.spring.io/spring-security/site/docs/current/reference/html/taglibs.html#the-authentication-tag) + - [Обработку 404 NotFound](https://stackoverflow.com/questions/18322279/spring-mvc-spring-security-and-error-handling) + - локализацию ошибок валидации + - проверки json в тестах через [JSONassert](https://github.com/skyscreamer/JSONassert) и [через jsonPath](https://www.petrikainulainen.net/programming/spring-framework/integration-testing-of-spring-mvc-applications-write-clean-assertions-with-jsonpath/) + - [логирование от Postgres Driver](http://stackoverflow.com/a/43242620/548473) + - в `.travis.yml` [сборку только ветки master](https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches) + - [защиту от кэширование ajax запросов в IE](https://stackoverflow.com/a/4303862/548473) + - обработку запрета модификации системный юзеров через универсальный `ApplicationException` + - рефакторинг + - сделал `@EntityGraph` через `attributePaths` + - реализаовал обработку дублирования `user.email` и `meal.dateTime` через [Controller Based Exception Handling](https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc#controller-based-exception-handling) + - поменял отключение транзакционности в тестах через `@Transactional(propagation = Propagation.NEVER)` + - сделал выбор в сервлете через switch + - [все логгирование сделал через {} форматирование](http://stackoverflow.com/questions/10555409/logger-slf4j-advantages-of-formatting-with-instead-of-string-concatenation) и поправил его в контроллерах (поле проверки id) + - [перешел на конструктор DI](http://stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it) + - в `ModelMatcher` переименовал `Comparator` -> `Equality` + - [заинлайнил все лямбды](http://stackoverflow.com/questions/19718353/is-repeatedly-instantiating-an-anonymous-class-wasteful) (компараторы, ModelMatcher.equality) + - поменялась реализация `JdbcUserRepositoryImpl.getAll()` + - на UI кнопки в таблице заменились на линки, поправил сообщения локализации + - [сделал кастомизацию JSON (@JsonView) и валидацию (groups)](https://drive.google.com/file/d/0B9Ye2auQ_NsFRTFsTjVHR2dXczA) для данных еды, отдаваемых на UI + - в `JdbcUserRepositoryImpl` поменял `MapSqlParameterSource` на `BeanPropertySqlParameterSource` +- удалил + - зависимость `javax.transaction.jta` (уже не нужна) + - `${spring.version}` в `pom.xml` зависимостях (уже есть в `spring-framework-bom`) + - distinct из запроса Hibernate на пользователей с ролями. [Оптимизация запроса distinct: 15.16.2](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct) + - лишние `
` тэги (`shadow` и `view-box`) + +### Topjava 9 +- добавил + - выбор профиля базы через `ActiveProfilesResolver`/`AllActiveProfileResolver` на основе драйвера базы в classpath + - видео Cascade. Auto generate DDL. + - проверку на правильность id в Ajax/Rest контроллерах (treat IDs in REST body) + - тесты на валидацию входных значений контроллеров и зависимость на имплементацию + - Bootstrap Glyphicons +- рефакторинг + - переименовал `TimeUtil` в `DateTimeUtil` + - переименовал `ExceptionUtil` в `ValidationUtil` + - заменил валидацию `@NotEmpty` на `@NotBlank` + - заменил `CascadeType.REMOVE` на `@OnDelete` + - изменил `JdbcUserRepositoryImpl.getAll()` + - обновил jQuery до 3.x, исключил из зависимостей webjars ненужные jQuery + - cделал загрузку скриптов асинхронной + - фильтр еды сделал в [Bootstrap Panels](http://getbootstrap.com/components/#panels) + - вместо `Persistable` ввел интерфейс `HasId` и наследую от него как Entity, так и TO + - сделал универсальную обработку исключений дублирования email и dateTime + +### Topjava 8 +- добавил: + - [защиту от XSS (Cross-Site Scripting)](http://stackoverflow.com/a/40644276/548473) + - интеграцию с Dependency Ci и Travis Ci + - локализацию календаря + - сводку по результатам тестов + - примеры запросов curl в `config/curl.md` + - DataTables/Bootstrap 3 integration + - тесты на профиль деплоя Heroku (общее количество JUnit тестов стало 102) +- удалил зависимость `jul-to-slf4j` +- рефакторинг + - переименовал все классы `UserMeal**` в `Meal**`, JSP + - переименовал `LoggedUser` в `AuthorizedUser` + - починил работа с PK Hibernate в случае ленивой загрузки (баг HHH-3718) + - поменял в `BaseEntity` `equals/hashCode/implements Persistable` + - в `InMemoryMealRepositoryImpl` выделил метод `getAllStream` + - перенес проверки пердусловий `Assert` из `InMemory` репозиториев в сервисы + - переименовал классы _Proxy*_ на более адекватные _Crud*_ + - поменял реализацию `JpaMealRepositoryImpl.get`, добавил в JPA модель `@BatchSize` + - вместо `@RequestMapping` ввел Spring 4.3 аннотации `@Get/Post/...Mapping` + - поменял авторизацию в тестах не-REST контроллеров + - перенес вызовы `UserUtil.prepareToSave` из `AbstractUserController` в `UserServiceImpl` + - зарефакторил обработку ошибок (`ExceptionInfoHandler`) + +### Topjava 7 +- добавил: + - [JPA 2.1 EntityGraph](https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs002.htm) + - [Jackson @JsonView](https://habrahabr.ru/post/307392/) + - валидацию объектов REST + - [i18n в JavaScript](http://stackoverflow.com/a/6242840/548473) + - проверку предусловий и видео Методы улучшения качества кода + - интеграцию с проверкой кода в Codacy + - [сравнение вермени исполнения запросов при различных meals индексах](https://drive.google.com/open?id=0B9Ye2auQ_NsFX3RLcnJCWmQ2Y0U) +- tomcat7-maven-plugin плагин перключили на Tomcat 8 (cargo-maven2-plugin) +- рефакторинг + - обработка ошибок сделал с array + - матчеров тестирования (сделал автоматические обертки и сравнение на основе передаваемого компаратора) + - вынес форматирование даты в `functions.tld` + +### Topjava 3-6 +- добавил + - [выпускной проект](https://drive.google.com/open?id=0B9Ye2auQ_NsFcG83dEVDVTVMamc) + - в таблицу meals составной индекс + - константы `Profiles.ACTIVE_DB`, `Profiles.DB_IMPLEMENTATION` + - проверки и тесты на `NotFound` для `UserMealService.getWithUser` и `UserService.getWithMeals` + - в MockMvc фильтр CharacterEncodingFilter + - защиту от межсайтовой подделки запроса, видео Межсайтовая подделка запроса (CSRF) + - ограничение на диапазон дат для фильтра еды +- рефакторинг + - UserMealsUtil, ProfileRestController, компараторов в репозитоии + - `LoggedUser` отнаследовал от `org.springframework.security.core.userdetails.User` + - переименовал `DbTest` в `AbstractServiceTest` и перенес сюда `@ActiveProfiles` + - сделал выполнение скриптов в тестах через аннотацию `@Sql` + - вместо использования id и селектора сделал обработчик `onclick` + - изменил формат ввода даты в форме без 'T' +- убрал + - `LoggerWrapper` + - Dandelion обертку к datatables +- обновил + - Hibernate до 5.x и Hibernate Validator, добавились новые зависимости и `jackson-datatype-hibernate5` + - datatables API (1.10) + - Postgres драйвер. Новый драйвер поддерживает Java 8 Time API, разделил реализацию JdbcMealRepositoryImpl на Java8 (Postgresql) и Timestamp (HSQL) diff --git a/cv.md b/cv.md new file mode 100644 index 000000000000..871af439801e --- /dev/null +++ b/cv.md @@ -0,0 +1,102 @@ +## Составление резюме, подготовка к интервью, поиск работы + +![cv](https://cloud.githubusercontent.com/assets/13649199/10877471/93ea86b8-8157-11e5-9bfa-95e3fba75c58.jpg) + +- Научиться программировать сложнее, чем кажется +- [Собеседование. Разработка ПО. Вопросы.](https://drive.google.com/open?id=0B9Ye2auQ_NsFQVc2WUdCR0xvLWM) +- [Набор ссылок для тренировки и прохождения интервью](https://github.com/andreis/interview) + +### Составление резюме: +- [VisualCV: create resume in minutes](https://www.visualcv.com/) +- Выбрать шаблон для резюме +- Как продать свое резюме в 2 раза дороже +- Как правильно составить резюме +- Резюме программистов. Часть 1 (плохие) +- Резюме программистов. Часть 2 (хорошие) +- Как составить резюме на английском +- ОФОРМЛЕНИЕ IT-РЕЗЮМЕ для USA + +### Наши истории (делимся опытом и успехом) + +### Тесты/задачи онлайн: +- [Java Programming Test](https://tests4geeks.com/java) +- game: test Java skills +- Codility lesson tests +- Quizful- тесты онлайн +- LeetCode Online Judge +- Sphere online judge +- Codility programmers lessons +- Hackerrank practice coding + +### Интервью: +- Михаил Портнов. Собеседование на работу: как продать себя грамотно +- Михаил Портнов. Какие вопросы мы задаем на собеседовании? +- Михаил Портнов. Собеседование на работу: жизненный путь +- Канал: Резюме, поиск работы, интервью +- Яков Файн: Как стать профессиональным Java разработчиком +- Ответы на вопросы на собеседовании Junior Java Developer +- Список вопросов с ответами для собеседования по Java +- Сборка по вопросам на интервью +- Сборка вопросов-ответов от JavaStudy +- Тест на знание 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: +- После завершения проекта вы освоите все заявленные в нем технологии - вставьте их в квалификацию (включая 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. + - отлично будет, если вы его еще как то сами доработаете + + +Делайте упор не на обучение, а на *участие в проекте*. Выполнение домашних заданий - это полноценное участие с написанием функционала по всем этим технологиям. На собеседовании меньше смотрят на то, что вы заканчивали, больше - на ваш опыт и то что вы знаете. +ОБЯЗАТЕЛЬНО УБЕРИТЕ любое упоминание Junior из резюме, количество обращений к вам возрастет на порядок. + +- Ссылку на стажировку можно поставить: http://javaops.ru (или в linkedin: https://www.linkedin.com/company/java-online-projects) + +Если рассмотриваете предложения по работе, **поднимите в своем профиле этот флаг и обязательно заполните ссылку на резюме: я запустил интеграцию с HR (более 100 контактов). Также проверьте содержание "Обо мне": по нему будут принимать решение открывать резюме или нет.** + +Ближе к концу курса я вышлю приглашение в нашу общую группу slack: общение на все темы IT, обсуждение вступительных заданий, вакансии, цены на рынке труда, события, помощь, интересные видео по теме и пр. + +### Подпишитесь на рассылку вакансий под себя + +### Основные сайты поиска работы: +- Яндекс агрегатор +- HH +- LinkedIn +- djinni.co (более актуально для Украины) + +## [Тестовое собеседование, самые спрашиваемые темы](http://javaops.ru/interview/test.html) +## Как выжить на испытательном сроке +- Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В + процессе формулирования очень часто приходит ее решение. +- Учись инвестигировать проблему. Внимательное чтение логов и умение дебажить - основные навыки + разработчика. В логах надо читать верх самого нижнего эксепшена - там причина всей портянки. +- Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и + бится нам ней часами. + Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время. +- Если тебе что-то объясняют по проекту - обязательно записывай. +- Когда получаешь задачу - уточни все очень подробно. +- Получай в процессе решения обратную связь - в том ли направлении ты идешь. +- Не игнорируй совместные ланчи (курилки) +- Готовься к стендапам/летучкам. Задавай на них вменяемые вопросы. Выказывай заинтересованность +- Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи. +- [**Советы новичкам**](http://blog.csssr.ru/2016/09/19/how-to-be-a-beginner-developer) + +## [Отзывы по стажировке Topjava](https://vk.com/topic-74381644_30447246) diff --git a/description.md b/description.md new file mode 100644 index 000000000000..d01afc2e22a7 --- /dev/null +++ b/description.md @@ -0,0 +1,76 @@ +#### Разработка полнофункционального 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), которые еще не поддерживаются ни JPA/Hibernate, ни Jackson/json). + Разбираются архитектурные паттерны: слои приложения и как правильно разбивать логику по слоям, когда нужно применять Data Transfer Object. + Т.е на выходе получается не учебный проект, а хорошо маштабируемый шаблон для большого проекта на всех пройденных технологиях. +- Большое внимание уделяется деталям: популяция базы, использование транзакционности, тесты сервисов и REST + контроллеров, насторойка EntityManagerFactory, + выбор реализации пула коннектов. Особое внимание уделяется работе с базой: через Spring JDBC, Spring ORM и + Spring Data Jpa. +- Используются самые востребованные на сегодняшний момент фреймворки: Maven, Spring Security 4 + вместе с Spring Security Test, наиболее удобный для работы с базой проект Spring Data Jpa, библиотека логирования logback, реализующая SLF4J, повсеместно используемый Bootstrap и jQuery. + +#### Демо разрабатываемого приложения + +## План проекта (ссылки на некоторые темы открыты для просмотра) +### Архитектура проекта. Персистентность. +- Системы управления версиями +- Java 8: Lambda, Stream API +- Обзор используемых в проекте технологий и инструментов. +- Инструмент сборки Maven. +- WAR. Веб-контейнер Tomcat. Сервлеты. +- Логирование. +- Обзор стандартных библиотек. Apache Commons, Guava +- Слои приложения. Создание каркаса приложения. +- Обзор Spring Framework. Spring Context. +- Тестирование через JUnit. +- Spring Test +- Базы данных. PostgreSQL. Обзор NoSQL и Java persistence solution без ORM. +- Настройка Database в IDEA. +- Скрипты инициализации базы. Spring Jdbc Template. +- Spring: инициализация и популирование DB +- ORM. Hibernate. JPA. +- Поддержка HSQLDB +- Транзакции +- Профили Maven и Spring +- Пул коннектов +- Spring Data JPA +- Кэш Hibernate + +### Разработка WEB +- Spring кэш +- Spring Web +- JSP, JSTL, i18n +- Tomcat maven plugin. JNDI +- Spring Web MVC +- Spring Internationalization +- Тестирование Spring MVC +- REST контроллеры +- Тестирование REST контроллеров. Jackson. +- jackson-datatype-hibernate. Тестирование через матчеры. +- Тестирование через SoapUi. UTF-8 +- WebJars. +- Bootstrap. jQuery datatables. +- AJAX. jQuery. Notifications. +- Spring Security +- Spring Binding/Validation +- Работа с datatables через Ajax. +- Spring Security Test +- [Кастомизация JSON (@JsonView) и валидации (groups)](https://drive.google.com/open?id=0B9Ye2auQ_NsFRTFsTjVHR2dXczA) +- Encoding password +- CSRF (добавление в проект защиты от межсайтовой подделки запроса) +- form-login. Spring Security Taglib +- Handler interceptor +- Spring Exception Handling +- Смена локали +- Фильтрация JSON через @JsonView +- Защита от XSS (Cross Site Scripting) +- Деплой в Heroku +- Локализация datatables, ошибок валидации +- Обработка ошибок 404 (NotFound) +- Доступ к AuthorizedUser +- Собеседование. Разработка ПО From 6fb74d3fed16414f984944baf3aed257d05fcc81 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 17 Sep 2017 16:40:58 +0300 Subject: [PATCH 002/139] prep --- Prepare_to_HW0.patch | 137 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 Prepare_to_HW0.patch diff --git a/Prepare_to_HW0.patch b/Prepare_to_HW0.patch new file mode 100644 index 000000000000..de0e3a4091f7 --- /dev/null +++ b/Prepare_to_HW0.patch @@ -0,0 +1,137 @@ +Index: src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java (revision ) ++++ src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java (revision ) +@@ -0,0 +1,35 @@ ++package ru.javawebinar.topjava.util; ++ ++import ru.javawebinar.topjava.model.UserMeal; ++import ru.javawebinar.topjava.model.UserMealWithExceed; ++ ++import java.time.LocalDateTime; ++import java.time.LocalTime; ++import java.time.Month; ++import java.util.Arrays; ++import java.util.List; ++ ++/** ++ * GKislin ++ * 31.05.2015. ++ */ ++public class UserMealsUtil { ++ public static void main(String[] args) { ++ List mealList = Arrays.asList( ++ new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,10,0), "Завтрак", 500), ++ new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,13,0), "Обед", 1000), ++ new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,20,0), "Ужин", 500), ++ new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,10,0), "Завтрак", 1000), ++ new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,13,0), "Обед", 500), ++ new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,20,0), "Ужин", 510) ++ ); ++ getFilteredWithExceeded(mealList, LocalTime.of(7, 0), LocalTime.of(12,0), 2000); ++// .toLocalDate(); ++// .toLocalTime(); ++ } ++ ++ public static List getFilteredWithExceeded(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { ++ // TODO return filtered list with correctly exceeded field ++ return null; ++ } ++} +Index: src/main/java/ru/javawebinar/topjava/util/TimeUtil.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- src/main/java/ru/javawebinar/topjava/util/TimeUtil.java (revision ) ++++ src/main/java/ru/javawebinar/topjava/util/TimeUtil.java (revision ) +@@ -0,0 +1,13 @@ ++package ru.javawebinar.topjava.util; ++ ++import java.time.LocalTime; ++ ++/** ++ * GKislin ++ * 07.01.2015. ++ */ ++public class TimeUtil { ++ public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { ++ return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; ++ } ++} +Index: src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java (revision ) ++++ src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java (revision ) +@@ -0,0 +1,24 @@ ++package ru.javawebinar.topjava.model; ++ ++import java.time.LocalDateTime; ++ ++/** ++ * GKislin ++ * 11.01.2015. ++ */ ++public class UserMealWithExceed { ++ private final LocalDateTime dateTime; ++ ++ private final String description; ++ ++ private final int calories; ++ ++ private final boolean exceed; ++ ++ public UserMealWithExceed(LocalDateTime dateTime, String description, int calories, boolean exceed) { ++ this.dateTime = dateTime; ++ this.description = description; ++ this.calories = calories; ++ this.exceed = exceed; ++ } ++} +Index: src/main/java/ru/javawebinar/topjava/model/UserMeal.java +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +--- src/main/java/ru/javawebinar/topjava/model/UserMeal.java (revision ) ++++ src/main/java/ru/javawebinar/topjava/model/UserMeal.java (revision ) +@@ -0,0 +1,33 @@ ++package ru.javawebinar.topjava.model; ++ ++import java.time.LocalDateTime; ++ ++/** ++ * GKislin ++ * 11.01.2015. ++ */ ++public class UserMeal { ++ private final LocalDateTime dateTime; ++ ++ private final String description; ++ ++ private final int calories; ++ ++ public UserMeal(LocalDateTime dateTime, String description, int calories) { ++ this.dateTime = dateTime; ++ this.description = description; ++ this.calories = calories; ++ } ++ ++ public LocalDateTime getDateTime() { ++ return dateTime; ++ } ++ ++ public String getDescription() { ++ return description; ++ } ++ ++ public int getCalories() { ++ return calories; ++ } ++} From 357668589b7d81aeb8a8196dbb65b6e88ebdbf28 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 17 Sep 2017 16:44:54 +0300 Subject: [PATCH 003/139] prep --- .../javawebinar/topjava/model/UserMeal.java | 33 +++++++++++++++++ .../topjava/model/UserMealWithExceed.java | 24 +++++++++++++ .../ru/javawebinar/topjava/util/TimeUtil.java | 13 +++++++ .../topjava/util/UserMealsUtil.java | 35 +++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/model/UserMeal.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/TimeUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java b/src/main/java/ru/javawebinar/topjava/model/UserMeal.java new file mode 100644 index 000000000000..aec9beb946cf --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/UserMeal.java @@ -0,0 +1,33 @@ +package ru.javawebinar.topjava.model; + +import java.time.LocalDateTime; + +/** + * GKislin + * 11.01.2015. + */ +public class UserMeal { + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + public UserMeal(LocalDateTime dateTime, String description, int calories) { + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java b/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java new file mode 100644 index 000000000000..f4f880e4e67a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.model; + +import java.time.LocalDateTime; + +/** + * GKislin + * 11.01.2015. + */ +public class UserMealWithExceed { + private final LocalDateTime dateTime; + + private final String description; + + private final int calories; + + private final boolean exceed; + + public UserMealWithExceed(LocalDateTime dateTime, String description, int calories, boolean exceed) { + this.dateTime = dateTime; + this.description = description; + this.calories = calories; + this.exceed = exceed; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java new file mode 100644 index 000000000000..02399b7288cf --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java @@ -0,0 +1,13 @@ +package ru.javawebinar.topjava.util; + +import java.time.LocalTime; + +/** + * GKislin + * 07.01.2015. + */ +public class TimeUtil { + public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { + return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java new file mode 100644 index 000000000000..8c5f1dc60142 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java @@ -0,0 +1,35 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.model.UserMealWithExceed; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +/** + * GKislin + * 31.05.2015. + */ +public class UserMealsUtil { + public static void main(String[] args) { + List mealList = Arrays.asList( + new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,10,0), "Завтрак", 500), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,13,0), "Обед", 1000), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,20,0), "Ужин", 500), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,10,0), "Завтрак", 1000), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,13,0), "Обед", 500), + new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,20,0), "Ужин", 510) + ); + getFilteredWithExceeded(mealList, LocalTime.of(7, 0), LocalTime.of(12,0), 2000); +// .toLocalDate(); +// .toLocalTime(); + } + + public static List getFilteredWithExceeded(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + // TODO return filtered list with correctly exceeded field + return null; + } +} From ac0a50c7eab6460fc7e89f81be19fe96308c9ff5 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 17 Sep 2017 16:48:45 +0300 Subject: [PATCH 004/139] Prepare to HW0 --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ca05c4a5184..fca72ef918de 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ out target *.iml log +*.patch From 8a9a2f46570d0431e86aaa2422daaa813c22c181 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 2 Nov 2017 21:42:28 +0300 Subject: [PATCH 005/139] 1_0 fix --- pom.xml | 2 +- .../java/ru/javawebinar/topjava/Main.java | 7 ++-- .../model/{UserMeal.java => Meal.java} | 8 ++--- ...ealWithExceed.java => MealWithExceed.java} | 8 ++--- .../javawebinar/topjava/util/MealsUtil.java | 31 ++++++++++++++++ .../topjava/util/UserMealsUtil.java | 35 ------------------- 6 files changed, 38 insertions(+), 53 deletions(-) rename src/main/java/ru/javawebinar/topjava/model/{UserMeal.java => Meal.java} (78%) rename src/main/java/ru/javawebinar/topjava/model/{UserMealWithExceed.java => MealWithExceed.java} (67%) create mode 100644 src/main/java/ru/javawebinar/topjava/util/MealsUtil.java delete mode 100644 src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java diff --git a/pom.xml b/pom.xml index c8a1c78f3b29..e3b8143e3e63 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 + 3.7.0 ${java.version} ${java.version} diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java index b23a2f0961fc..cb7e35af6afa 100644 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ b/src/main/java/ru/javawebinar/topjava/Main.java @@ -1,11 +1,8 @@ package ru.javawebinar.topjava; /** - * User: gkislin - * Date: 05.08.2015 - * - * @link http://caloriesmng.herokuapp.com/ - * @link https://github.com/JavaOPs/topjava + * @see Demo + * @see Initial project */ public class Main { public static void main(String[] args) { diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java similarity index 78% rename from src/main/java/ru/javawebinar/topjava/model/UserMeal.java rename to src/main/java/ru/javawebinar/topjava/model/Meal.java index aec9beb946cf..f546cef0f74a 100644 --- a/src/main/java/ru/javawebinar/topjava/model/UserMeal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -2,18 +2,14 @@ import java.time.LocalDateTime; -/** - * GKislin - * 11.01.2015. - */ -public class UserMeal { +public class Meal { private final LocalDateTime dateTime; private final String description; private final int calories; - public UserMeal(LocalDateTime dateTime, String description, int calories) { + public Meal(LocalDateTime dateTime, String description, int calories) { this.dateTime = dateTime; this.description = description; this.calories = calories; diff --git a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java similarity index 67% rename from src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java rename to src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java index f4f880e4e67a..c8e0d713c217 100644 --- a/src/main/java/ru/javawebinar/topjava/model/UserMealWithExceed.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java @@ -2,11 +2,7 @@ import java.time.LocalDateTime; -/** - * GKislin - * 11.01.2015. - */ -public class UserMealWithExceed { +public class MealWithExceed { private final LocalDateTime dateTime; private final String description; @@ -15,7 +11,7 @@ public class UserMealWithExceed { private final boolean exceed; - public UserMealWithExceed(LocalDateTime dateTime, String description, int calories, boolean exceed) { + public MealWithExceed(LocalDateTime dateTime, String description, int calories, boolean exceed) { this.dateTime = dateTime; this.description = description; this.calories = calories; 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..a1bb183937ef --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -0,0 +1,31 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.MealWithExceed; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +public class MealsUtil { + public static void main(String[] args) { + List mealList = Arrays.asList( + new Meal(LocalDateTime.of(2015, Month.MAY, 30,10,0), "Завтрак", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 30,13,0), "Обед", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 30,20,0), "Ужин", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31,10,0), "Завтрак", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 31,13,0), "Обед", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31,20,0), "Ужин", 510) + ); + getFilteredWithExceeded(mealList, LocalTime.of(7, 0), LocalTime.of(12,0), 2000); +// .toLocalDate(); +// .toLocalTime(); + } + + public static List getFilteredWithExceeded(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + // TODO return filtered list with correctly exceeded field + return null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java deleted file mode 100644 index 8c5f1dc60142..000000000000 --- a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package ru.javawebinar.topjava.util; - -import ru.javawebinar.topjava.model.UserMeal; -import ru.javawebinar.topjava.model.UserMealWithExceed; - -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.Month; -import java.util.Arrays; -import java.util.List; - -/** - * GKislin - * 31.05.2015. - */ -public class UserMealsUtil { - public static void main(String[] args) { - List mealList = Arrays.asList( - new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,10,0), "Завтрак", 500), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,13,0), "Обед", 1000), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 30,20,0), "Ужин", 500), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,10,0), "Завтрак", 1000), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,13,0), "Обед", 500), - new UserMeal(LocalDateTime.of(2015, Month.MAY, 31,20,0), "Ужин", 510) - ); - getFilteredWithExceeded(mealList, LocalTime.of(7, 0), LocalTime.of(12,0), 2000); -// .toLocalDate(); -// .toLocalTime(); - } - - public static List getFilteredWithExceeded(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - // TODO return filtered list with correctly exceeded field - return null; - } -} From 5bfa4e7746499e17cf668cdaf6818a1492104b04 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 2 Nov 2017 21:43:08 +0300 Subject: [PATCH 006/139] 1_1 HW0 stream --- .../ru/javawebinar/topjava/model/Meal.java | 10 +++++ .../topjava/model/MealWithExceed.java | 12 +++++- .../javawebinar/topjava/util/MealsUtil.java | 40 ++++++++++++------- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index f546cef0f74a..943ff5cd59fa 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.model; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; public class Meal { private final LocalDateTime dateTime; @@ -26,4 +28,12 @@ public String getDescription() { public int getCalories() { return calories; } + + public LocalDate getDate() { + return dateTime.toLocalDate(); + } + + public LocalTime getTime() { + return dateTime.toLocalTime(); + } } diff --git a/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java index c8e0d713c217..4751c9e4fd69 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java @@ -17,4 +17,14 @@ public MealWithExceed(LocalDateTime dateTime, String description, int calories, this.calories = calories; this.exceed = exceed; } -} + + @Override + public String toString() { + return "UserMealWithExceed{" + + "dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + ", exceed=" + exceed + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index a1bb183937ef..2143f7f49faa 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -3,29 +3,41 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.model.MealWithExceed; +import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; public class MealsUtil { public static void main(String[] args) { - List mealList = Arrays.asList( - new Meal(LocalDateTime.of(2015, Month.MAY, 30,10,0), "Завтрак", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 30,13,0), "Обед", 1000), - new Meal(LocalDateTime.of(2015, Month.MAY, 30,20,0), "Ужин", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 31,10,0), "Завтрак", 1000), - new Meal(LocalDateTime.of(2015, Month.MAY, 31,13,0), "Обед", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 31,20,0), "Ужин", 510) + List meals = Arrays.asList( + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) ); - getFilteredWithExceeded(mealList, LocalTime.of(7, 0), LocalTime.of(12,0), 2000); -// .toLocalDate(); -// .toLocalTime(); + List mealsWithExceeded = getFilteredWithExceeded(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); + mealsWithExceeded.forEach(System.out::println); } - public static List getFilteredWithExceeded(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { - // TODO return filtered list with correctly exceeded field - return null; + public static List getFilteredWithExceeded(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + Map caloriesSumByDate = meals.stream() + .collect( + Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) +// Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum) + ); + + return meals.stream() + .filter(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime)) + .map(meal -> + new MealWithExceed(meal.getDateTime(), meal.getDescription(), meal.getCalories(), + caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .collect(Collectors.toList()); } -} +} \ No newline at end of file From b67f3a6b12f6aae43f424ef234580f341848983c Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 2 Nov 2017 21:46:59 +0300 Subject: [PATCH 007/139] 1_2 HW0 cycle --- .../javawebinar/topjava/util/MealsUtil.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 2143f7f49faa..2ef2768e90bc 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -7,9 +7,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; public class MealsUtil { @@ -24,6 +22,8 @@ public static void main(String[] args) { ); List mealsWithExceeded = getFilteredWithExceeded(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); mealsWithExceeded.forEach(System.out::println); + + System.out.println(getFilteredWithExceededByCycle(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); } public static List getFilteredWithExceeded(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { @@ -35,9 +35,25 @@ public static List getFilteredWithExceeded(List meals, Loc return meals.stream() .filter(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime)) - .map(meal -> - new MealWithExceed(meal.getDateTime(), meal.getDescription(), meal.getCalories(), - caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) + .map(meal -> createWithExceed(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) .collect(Collectors.toList()); } + + public static List getFilteredWithExceededByCycle(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + + final Map caloriesSumByDate = new HashMap<>(); + meals.forEach(meal -> caloriesSumByDate.merge(meal.getDate(), meal.getCalories(), Integer::sum)); + + final List mealsWithExceeded = new ArrayList<>(); + meals.forEach(meal -> { + if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { + mealsWithExceeded.add(createWithExceed(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)); + } + }); + return mealsWithExceeded; + } + + public static MealWithExceed createWithExceed(Meal meal, boolean exceeded) { + return new MealWithExceed(meal.getDateTime(), meal.getDescription(), meal.getCalories(), exceeded); + } } \ No newline at end of file From cb6e9905d609d3fedf41e2ca57629713e936a292 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 2 Nov 2017 21:48:14 +0300 Subject: [PATCH 008/139] 1_3 HW0 optional2 --- .../javawebinar/topjava/util/MealsUtil.java | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 2ef2768e90bc..209284416ff6 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -8,7 +8,12 @@ import java.time.LocalTime; import java.time.Month; import java.util.*; +import java.util.stream.Collector; import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toList; public class MealsUtil { public static void main(String[] args) { @@ -24,6 +29,8 @@ public static void main(String[] args) { mealsWithExceeded.forEach(System.out::println); System.out.println(getFilteredWithExceededByCycle(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); + System.out.println(getFilteredWithExceededInOnePass(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); + System.out.println(getFilteredWithExceededInOnePass2(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); } public static List getFilteredWithExceeded(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { @@ -36,7 +43,7 @@ public static List getFilteredWithExceeded(List meals, Loc return meals.stream() .filter(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime)) .map(meal -> createWithExceed(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) - .collect(Collectors.toList()); + .collect(toList()); } public static List getFilteredWithExceededByCycle(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { @@ -53,6 +60,51 @@ public static List getFilteredWithExceededByCycle(List mea return mealsWithExceeded; } + public static List getFilteredWithExceededInOnePass(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + Collection> list = meals.stream() + .collect(Collectors.groupingBy(Meal::getDate)).values(); + + return list.stream().flatMap(dayMeals -> { + boolean exceed = dayMeals.stream().mapToInt(Meal::getCalories).sum() > caloriesPerDay; + return dayMeals.stream().filter(meal -> + TimeUtil.isBetween(meal.getTime(), startTime, endTime)) + .map(meal -> createWithExceed(meal, exceed)); + }).collect(toList()); + } + + public static List getFilteredWithExceededInOnePass2(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + final class Aggregate { + private final List dailyMeals = new ArrayList<>(); + private int dailySumOfCalories; + + private void accumulate(Meal meal) { + dailySumOfCalories += meal.getCalories(); + if (TimeUtil.isBetween(meal.getDateTime().toLocalTime(), startTime, endTime)) { + dailyMeals.add(meal); + } + } + + // never invoked if the upstream is sequential + private Aggregate combine(Aggregate that) { + this.dailySumOfCalories += that.dailySumOfCalories; + this.dailyMeals.addAll(that.dailyMeals); + return this; + } + + private Stream finisher() { + final boolean exceed = dailySumOfCalories > caloriesPerDay; + return dailyMeals.stream().map(meal -> createWithExceed(meal, exceed)); + } + } + + Collection> values = meals.stream() + .collect(Collectors.groupingBy(Meal::getDate, + Collector.of(Aggregate::new, Aggregate::accumulate, Aggregate::combine, Aggregate::finisher)) + ).values(); + + return values.stream().flatMap(identity()).collect(toList()); + } + public static MealWithExceed createWithExceed(Meal meal, boolean exceeded) { return new MealWithExceed(meal.getDateTime(), meal.getDescription(), meal.getCalories(), exceeded); } From 8fe5986be6e2e8e3357192a2fb0aad856d46b597 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 2 Nov 2017 21:55:23 +0300 Subject: [PATCH 009/139] 1_4 switch to war --- pom.xml | 2 +- src/main/webapp/WEB-INF/web.xml | 19 +++++++++++++++++++ src/main/webapp/index.html | 13 +++++++++++++ src/main/webapp/users.jsp | 10 ++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/index.html create mode 100644 src/main/webapp/users.jsp diff --git a/pom.xml b/pom.xml index e3b8143e3e63..2e87eaf4e859 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ru.javawebinar topjava - jar + war 1.0-SNAPSHOT diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..1c91ffe2cbaf --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,19 @@ + + + Topjava + + + userServlet + ru.javawebinar.topjava.web.UserServlet + 0 + + + userServlet + /users + + + diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html new file mode 100644 index 000000000000..6253517f8b84 --- /dev/null +++ b/src/main/webapp/index.html @@ -0,0 +1,13 @@ + + + + Java Enterprise (Topjava) + + +

Проект Java Enterprise (Topjava)

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

Home

+

Users

+ + \ No newline at end of file From fbb3df819a28984b16aa3c88b7ccbac69419ffc2 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 2 Nov 2017 21:56:12 +0300 Subject: [PATCH 010/139] 1_5 add servlet api --- pom.xml | 8 ++++++++ .../ru/javawebinar/topjava/web/UserServlet.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/web/UserServlet.java diff --git a/pom.xml b/pom.xml index 2e87eaf4e859..abd5d2a6b91e 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,14 @@ + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java new file mode 100644 index 000000000000..76056e06c019 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -0,0 +1,15 @@ +package ru.javawebinar.topjava.web; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class UserServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + request.getRequestDispatcher("/users.jsp").forward(request, response); + } +} From a9012b5f35fb7a8b7ddac1881b01d6a33a2b00aa Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 2 Nov 2017 21:57:16 +0300 Subject: [PATCH 011/139] 1_6 forward to redirect --- pom.xml | 2 +- src/main/java/ru/javawebinar/topjava/web/UserServlet.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index abd5d2a6b91e..010a0c933e95 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ topjava - install + package org.apache.maven.plugins diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index 76056e06c019..11f282bac482 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -10,6 +10,7 @@ public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - request.getRequestDispatcher("/users.jsp").forward(request, response); +// request.getRequestDispatcher("/users.jsp").forward(request, response); + response.sendRedirect("users.jsp"); } } From acd724f0539b56f6489a7c404903f197e960549a Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 2 Nov 2017 21:58:12 +0300 Subject: [PATCH 012/139] 1_7 logging --- pom.xml | 25 ++++++++++++++++ .../javawebinar/topjava/web/UserServlet.java | 7 +++++ src/main/resources/logback.xml | 29 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 src/main/resources/logback.xml diff --git a/pom.xml b/pom.xml index 010a0c933e95..d2ac57ffd6f5 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,10 @@ 1.8 UTF-8 UTF-8 + + + 1.2.3 + 1.7.25 @@ -34,6 +38,27 @@ + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + runtime + + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index 11f282bac482..ef52d67576c0 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -1,15 +1,22 @@ package ru.javawebinar.topjava.web; +import org.slf4j.Logger; + import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import static org.slf4j.LoggerFactory.getLogger; + public class UserServlet extends HttpServlet { + private static final Logger log = getLogger(UserServlet.class); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + log.debug("redirect to users"); + // request.getRequestDispatcher("/users.jsp").forward(request, response); response.sendRedirect("users.jsp"); } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 000000000000..e9b900b26669 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,29 @@ + + + + + + + + ${TOPJAVA_ROOT}/log/topjava.log + + + UTF-8 + %date %-5level %logger{0} [%file:%line] %msg%n + + + + + + UTF-8 + %-5level %logger{0} [%file:%line] %msg%n + + + + + + + + + + From bfe0d5e0304827364b79d4aee51bfeed1d84b73a Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 9 Nov 2017 21:08:35 +0300 Subject: [PATCH 013/139] 2_0 patch --- src/main/java/ru/javawebinar/topjava/Main.java | 11 ----------- .../ru/javawebinar/topjava/model/MealWithExceed.java | 2 +- .../java/ru/javawebinar/topjava/util/MealsUtil.java | 2 +- .../java/ru/javawebinar/topjava/util/TimeUtil.java | 4 ---- 4 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 src/main/java/ru/javawebinar/topjava/Main.java diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java deleted file mode 100644 index cb7e35af6afa..000000000000 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.javawebinar.topjava; - -/** - * @see Demo - * @see Initial project - */ -public class Main { - public static void main(String[] args) { - System.out.format("Hello Topjava Enterprise!"); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java index 4751c9e4fd69..438ad5e2b800 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java @@ -20,7 +20,7 @@ public MealWithExceed(LocalDateTime dateTime, String description, int calories, @Override public String toString() { - return "UserMealWithExceed{" + + return "MealWithExceed{" + "dateTime=" + dateTime + ", description='" + description + '\'' + ", calories=" + calories + diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 209284416ff6..7fc4ed1eb4c5 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -105,7 +105,7 @@ private Stream finisher() { return values.stream().flatMap(identity()).collect(toList()); } - public static MealWithExceed createWithExceed(Meal meal, boolean exceeded) { + private static MealWithExceed createWithExceed(Meal meal, boolean exceeded) { return new MealWithExceed(meal.getDateTime(), meal.getDescription(), meal.getCalories(), exceeded); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java index 02399b7288cf..b7eb2af6f93e 100644 --- a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java @@ -2,10 +2,6 @@ import java.time.LocalTime; -/** - * GKislin - * 07.01.2015. - */ public class TimeUtil { public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; From 5399577037d31650b28c785c8f883aa381eff8ec Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 9 Nov 2017 21:12:42 +0300 Subject: [PATCH 014/139] 2_1 HW01 --- pom.xml | 6 +++ .../topjava/model/MealWithExceed.java | 16 ++++++ .../topjava/util/DateTimeUtil.java | 17 ++++++ .../javawebinar/topjava/util/MealsUtil.java | 53 +++++++++---------- .../ru/javawebinar/topjava/util/TimeUtil.java | 9 ---- .../javawebinar/topjava/web/MealServlet.java | 22 ++++++++ .../javawebinar/topjava/web/UserServlet.java | 6 +-- src/main/webapp/WEB-INF/tld/functions.tld | 16 ++++++ src/main/webapp/WEB-INF/web.xml | 10 ++++ src/main/webapp/index.html | 1 + src/main/webapp/meals.jsp | 48 +++++++++++++++++ 11 files changed, 164 insertions(+), 40 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java delete mode 100644 src/main/java/ru/javawebinar/topjava/util/TimeUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/MealServlet.java create mode 100644 src/main/webapp/WEB-INF/tld/functions.tld create mode 100644 src/main/webapp/meals.jsp diff --git a/pom.xml b/pom.xml index d2ac57ffd6f5..6f484d0beec9 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,12 @@ 3.1.0 provided + + + javax.servlet + jstl + 1.2 + diff --git a/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java index 438ad5e2b800..b2ab232a03e8 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java @@ -18,6 +18,22 @@ public MealWithExceed(LocalDateTime dateTime, String description, int calories, this.exceed = exceed; } + public LocalDateTime getDateTime() { + return dateTime; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public boolean isExceed() { + return exceed; + } + @Override public String toString() { return "MealWithExceed{" + 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..5de28849657a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -0,0 +1,17 @@ +package ru.javawebinar.topjava.util; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +public class DateTimeUtil { + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { + return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; + } + + public static String toString(LocalDateTime ldt) { + return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 7fc4ed1eb4c5..30f0dcee1df6 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -7,30 +7,27 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; -import java.util.*; -import java.util.stream.Collector; +import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; -import java.util.stream.Stream; -import static java.util.function.Function.identity; import static java.util.stream.Collectors.toList; public class MealsUtil { - public static void main(String[] args) { - List meals = Arrays.asList( - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), - new Meal(LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 1000), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), - new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) - ); - List mealsWithExceeded = getFilteredWithExceeded(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); - mealsWithExceeded.forEach(System.out::println); - - System.out.println(getFilteredWithExceededByCycle(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); - System.out.println(getFilteredWithExceededInOnePass(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); - System.out.println(getFilteredWithExceededInOnePass2(meals, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); + public static final List MEALS = Arrays.asList( + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 1000), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), + new Meal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510) + ); + + public static final int DEFAULT_CALORIES_PER_DAY = 2000; + + public static List getWithExceeded(List meals, int caloriesPerDay) { + return getFilteredWithExceeded(meals, LocalTime.MIN, LocalTime.MAX, caloriesPerDay); } public static List getFilteredWithExceeded(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { @@ -41,11 +38,16 @@ public static List getFilteredWithExceeded(List meals, Loc ); return meals.stream() - .filter(meal -> TimeUtil.isBetween(meal.getTime(), startTime, endTime)) + .filter(meal -> DateTimeUtil.isBetween(meal.getTime(), startTime, endTime)) .map(meal -> createWithExceed(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)) .collect(toList()); } + private static MealWithExceed createWithExceed(Meal meal, boolean exceeded) { + return new MealWithExceed(meal.getDateTime(), meal.getDescription(), meal.getCalories(), exceeded); + } + +/* public static List getFilteredWithExceededByCycle(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { final Map caloriesSumByDate = new HashMap<>(); @@ -53,7 +55,7 @@ public static List getFilteredWithExceededByCycle(List mea final List mealsWithExceeded = new ArrayList<>(); meals.forEach(meal -> { - if (TimeUtil.isBetween(meal.getTime(), startTime, endTime)) { + if (DateTimeUtil.isBetween(meal.getTime(), startTime, endTime)) { mealsWithExceeded.add(createWithExceed(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay)); } }); @@ -67,7 +69,7 @@ public static List getFilteredWithExceededInOnePass(List m return list.stream().flatMap(dayMeals -> { boolean exceed = dayMeals.stream().mapToInt(Meal::getCalories).sum() > caloriesPerDay; return dayMeals.stream().filter(meal -> - TimeUtil.isBetween(meal.getTime(), startTime, endTime)) + DateTimeUtil.isBetween(meal.getTime(), startTime, endTime)) .map(meal -> createWithExceed(meal, exceed)); }).collect(toList()); } @@ -79,7 +81,7 @@ final class Aggregate { private void accumulate(Meal meal) { dailySumOfCalories += meal.getCalories(); - if (TimeUtil.isBetween(meal.getDateTime().toLocalTime(), startTime, endTime)) { + if (DateTimeUtil.isBetween(meal.getDateTime().toLocalTime(), startTime, endTime)) { dailyMeals.add(meal); } } @@ -104,8 +106,5 @@ private Stream finisher() { return values.stream().flatMap(identity()).collect(toList()); } - - private static MealWithExceed createWithExceed(Meal meal, boolean exceeded) { - return new MealWithExceed(meal.getDateTime(), meal.getDescription(), meal.getCalories(), exceeded); - } +*/ } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java deleted file mode 100644 index b7eb2af6f93e..000000000000 --- a/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.javawebinar.topjava.util; - -import java.time.LocalTime; - -public class TimeUtil { - public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { - return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java new file mode 100644 index 000000000000..672ea881707e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -0,0 +1,22 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.util.MealsUtil; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MealServlet extends HttpServlet { + private static final Logger log = LoggerFactory.getLogger(MealServlet.class); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + log.info("getAll"); + request.setAttribute("meals", MealsUtil.getWithExceeded(MealsUtil.MEALS, MealsUtil.DEFAULT_CALORIES_PER_DAY)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java index ef52d67576c0..f6cf12e69976 100644 --- a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java @@ -15,9 +15,7 @@ public class UserServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.debug("redirect to users"); - -// request.getRequestDispatcher("/users.jsp").forward(request, response); - response.sendRedirect("users.jsp"); + log.debug("forward to users"); + request.getRequestDispatcher("/users.jsp").forward(request, response); } } diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld new file mode 100644 index 000000000000..d138fecdbfb5 --- /dev/null +++ b/src/main/webapp/WEB-INF/tld/functions.tld @@ -0,0 +1,16 @@ + + + + 1.0 + functions + http://topjava.javawebinar.ru/functions + + + formatDateTime + ru.javawebinar.topjava.util.DateTimeUtil + java.lang.String toString(java.time.LocalDateTime) + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 1c91ffe2cbaf..d2e475517f7e 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -16,4 +16,14 @@ /users + + mealServlet + ru.javawebinar.topjava.web.MealServlet + 0 + + + mealServlet + /meals + + diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html index 6253517f8b84..cd88b335a454 100644 --- a/src/main/webapp/index.html +++ b/src/main/webapp/index.html @@ -8,6 +8,7 @@

Проект Users +
  • Meals
  • diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp new file mode 100644 index 000000000000..9da96c60321b --- /dev/null +++ b/src/main/webapp/meals.jsp @@ -0,0 +1,48 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="fn" uri="http://topjava.javawebinar.ru/functions" %> +<%--<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>--%> + + + Meal list + + + +
    +

    Home

    +

    Meals

    +
    + + + + + + + + + + + + + + + + +
    DateDescriptionCalories
    + <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> + <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> + <%--${fn:replace(meal.dateTime, 'T', ' ')}--%> + ${fn:formatDateTime(meal.dateTime)} + ${meal.description}${meal.calories}
    +
    + + \ No newline at end of file From 848eb1b9f6380ffd77d1ab6fea1641e9e43a7c0a Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 9 Nov 2017 21:13:33 +0300 Subject: [PATCH 015/139] 2_1 HW01 Optional --- .../ru/javawebinar/topjava/model/Meal.java | 29 +++++++++ .../topjava/model/MealWithExceed.java | 12 +++- .../InMemoryMealRepositoryImpl.java | 43 +++++++++++++ .../topjava/repository/MealRepository.java | 15 +++++ .../javawebinar/topjava/util/MealsUtil.java | 7 ++- .../javawebinar/topjava/web/MealServlet.java | 63 ++++++++++++++++++- src/main/webapp/mealForm.jsp | 51 +++++++++++++++ src/main/webapp/meals.jsp | 5 ++ 8 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepositoryImpl.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/MealRepository.java create mode 100644 src/main/webapp/mealForm.jsp diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 943ff5cd59fa..3abbee42511e 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -5,6 +5,8 @@ import java.time.LocalTime; public class Meal { + private Integer id; + private final LocalDateTime dateTime; private final String description; @@ -12,11 +14,24 @@ public class Meal { private final int calories; public Meal(LocalDateTime dateTime, String description, int calories) { + this(null, dateTime, description, calories); + } + + public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { + this.id = id; this.dateTime = dateTime; this.description = description; this.calories = calories; } + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + public LocalDateTime getDateTime() { return dateTime; } @@ -36,4 +51,18 @@ public LocalDate getDate() { public LocalTime getTime() { return dateTime.toLocalTime(); } + + public boolean isNew() { + return id == null; + } + + @Override + public String toString() { + return "Meal{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + '}'; + } } diff --git a/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java index b2ab232a03e8..2b375e45eecc 100644 --- a/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java +++ b/src/main/java/ru/javawebinar/topjava/model/MealWithExceed.java @@ -3,6 +3,8 @@ import java.time.LocalDateTime; public class MealWithExceed { + private final Integer id; + private final LocalDateTime dateTime; private final String description; @@ -11,13 +13,18 @@ public class MealWithExceed { private final boolean exceed; - public MealWithExceed(LocalDateTime dateTime, String description, int calories, boolean exceed) { + public MealWithExceed(Integer id, LocalDateTime dateTime, String description, int calories, boolean exceed) { + this.id = id; this.dateTime = dateTime; this.description = description; this.calories = calories; this.exceed = exceed; } + public Integer getId() { + return id; + } + public LocalDateTime getDateTime() { return dateTime; } @@ -37,7 +44,8 @@ public boolean isExceed() { @Override public String toString() { return "MealWithExceed{" + - "dateTime=" + dateTime + + "id=" + id + + ", dateTime=" + dateTime + ", description='" + description + '\'' + ", calories=" + calories + ", exceed=" + exceed + diff --git a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepositoryImpl.java new file mode 100644 index 000000000000..bcb9cd8894a0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepositoryImpl.java @@ -0,0 +1,43 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.util.MealsUtil; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class InMemoryMealRepositoryImpl implements MealRepository { + private Map repository = new ConcurrentHashMap<>(); + private AtomicInteger counter = new AtomicInteger(0); + + { + MealsUtil.MEALS.forEach(this::save); + } + + @Override + public Meal save(Meal meal) { + if (meal.isNew()) { + meal.setId(counter.incrementAndGet()); + } + repository.put(meal.getId(), meal); + return meal; + } + + @Override + public void delete(int id) { + repository.remove(id); + } + + @Override + public Meal get(int id) { + return repository.get(id); + } + + @Override + public Collection getAll() { + return repository.values(); + } +} + diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java new file mode 100644 index 000000000000..e249a885c8bf --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java @@ -0,0 +1,15 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.Meal; + +import java.util.Collection; + +public interface MealRepository { + Meal save(Meal meal); + + void delete(int id); + + Meal get(int id); + + Collection getAll(); +} diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 30f0dcee1df6..81e76b78b188 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -8,6 +8,7 @@ import java.time.LocalTime; import java.time.Month; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -26,11 +27,11 @@ public class MealsUtil { public static final int DEFAULT_CALORIES_PER_DAY = 2000; - public static List getWithExceeded(List meals, int caloriesPerDay) { + public static List getWithExceeded(Collection meals, int caloriesPerDay) { return getFilteredWithExceeded(meals, LocalTime.MIN, LocalTime.MAX, caloriesPerDay); } - public static List getFilteredWithExceeded(List meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + public static List getFilteredWithExceeded(Collection meals, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { Map caloriesSumByDate = meals.stream() .collect( Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories)) @@ -44,7 +45,7 @@ public static List getFilteredWithExceeded(List meals, Loc } private static MealWithExceed createWithExceed(Meal meal, boolean exceeded) { - return new MealWithExceed(meal.getDateTime(), meal.getDescription(), meal.getCalories(), exceeded); + return new MealWithExceed(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), exceeded); } /* diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 672ea881707e..065bb4ff8d81 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -2,21 +2,78 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.InMemoryMealRepositoryImpl; +import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; +import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Objects; public class MealServlet extends HttpServlet { private static final Logger log = LoggerFactory.getLogger(MealServlet.class); + private MealRepository repository; + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + repository = new InMemoryMealRepositoryImpl(); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + request.setCharacterEncoding("UTF-8"); + String id = request.getParameter("id"); + + Meal meal = new Meal(id.isEmpty() ? null : Integer.valueOf(id), + LocalDateTime.parse(request.getParameter("dateTime")), + request.getParameter("description"), + Integer.valueOf(request.getParameter("calories"))); + + log.info(meal.isNew() ? "Create {}" : "Update {}", meal); + repository.save(meal); + response.sendRedirect("meals"); + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - log.info("getAll"); - request.setAttribute("meals", MealsUtil.getWithExceeded(MealsUtil.MEALS, MealsUtil.DEFAULT_CALORIES_PER_DAY)); - request.getRequestDispatcher("/meals.jsp").forward(request, response); + String action = request.getParameter("action"); + + switch (action == null ? "all" : action) { + case "delete": + int id = getId(request); + log.info("Delete {}", id); + repository.delete(id); + response.sendRedirect("meals"); + break; + case "create": + case "update": + final Meal meal = "create".equals(action) ? + new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) : + repository.get(getId(request)); + request.setAttribute("meal", meal); + request.getRequestDispatcher("/mealForm.jsp").forward(request, response); + break; + case "all": + default: + log.info("getAll"); + request.setAttribute("meals", + MealsUtil.getWithExceeded(repository.getAll(), MealsUtil.DEFAULT_CALORIES_PER_DAY)); + request.getRequestDispatcher("/meals.jsp").forward(request, response); + break; + } + } + + private int getId(HttpServletRequest request) { + String paramId = Objects.requireNonNull(request.getParameter("id")); + return Integer.valueOf(paramId); } } diff --git a/src/main/webapp/mealForm.jsp b/src/main/webapp/mealForm.jsp new file mode 100644 index 000000000000..3fa1f71e66c1 --- /dev/null +++ b/src/main/webapp/mealForm.jsp @@ -0,0 +1,51 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + Meal + + + +
    +

    Home

    +

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

    +
    + +
    + +
    +
    DateTime:
    +
    +
    +
    +
    Description:
    +
    +
    +
    +
    Calories:
    +
    +
    + + +
    +
    + + diff --git a/src/main/webapp/meals.jsp b/src/main/webapp/meals.jsp index 9da96c60321b..8152fcf68c64 100644 --- a/src/main/webapp/meals.jsp +++ b/src/main/webapp/meals.jsp @@ -20,6 +20,7 @@

    Home

    Meals

    + Add Meal
    @@ -27,6 +28,8 @@ + + @@ -40,6 +43,8 @@ + +
    Date Description Calories
    ${meal.description} ${meal.calories}UpdateDelete
    From ff04189b99a6c9728bfb17c192fa92940c0966cf Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 9 Nov 2017 21:14:21 +0300 Subject: [PATCH 016/139] 2_3 app_layers --- .../javawebinar/topjava/AuthorizedUser.java | 14 +++ .../topjava/model/AbstractBaseEntity.java | 26 ++++++ .../topjava/model/AbstractNamedEntity.java | 24 +++++ .../ru/javawebinar/topjava/model/Role.java | 6 ++ .../ru/javawebinar/topjava/model/User.java | 91 +++++++++++++++++++ .../topjava/repository/UserRepository.java | 20 ++++ .../topjava/service/MealService.java | 4 + .../topjava/service/MealServiceImpl.java | 9 ++ .../topjava/service/UserService.java | 22 +++++ .../topjava/service/UserServiceImpl.java | 45 +++++++++ .../topjava/util/ValidationUtil.java | 42 +++++++++ .../util/exception/NotFoundException.java | 7 ++ .../topjava/web/meal/MealRestController.java | 8 ++ .../web/user/AbstractUserController.java | 49 ++++++++++ .../topjava/web/user/AdminRestController.java | 38 ++++++++ .../web/user/ProfileRestController.java | 19 ++++ 16 files changed, 424 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/AuthorizedUser.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/Role.java create mode 100644 src/main/java/ru/javawebinar/topjava/model/User.java create mode 100644 src/main/java/ru/javawebinar/topjava/repository/UserRepository.java create mode 100644 src/main/java/ru/javawebinar/topjava/service/MealService.java create mode 100644 src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java create mode 100644 src/main/java/ru/javawebinar/topjava/service/UserService.java create mode 100644 src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java diff --git a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java new file mode 100644 index 000000000000..177a05931540 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java @@ -0,0 +1,14 @@ +package ru.javawebinar.topjava; + +import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; + +public class AuthorizedUser { + + public static int id() { + return 1; + } + + public static int getCaloriesPerDay() { + return DEFAULT_CALORIES_PER_DAY; + } +} \ 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..a3d71fcb46ed --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -0,0 +1,26 @@ +package ru.javawebinar.topjava.model; + +public abstract class AbstractBaseEntity { + protected Integer id; + + protected AbstractBaseEntity(Integer id) { + this.id = id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public boolean isNew() { + return this.id == null; + } + + @Override + public String toString() { + return String.format("Entity %s (%s)", getClass().getName(), 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..259511dd0b65 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.model; + +public abstract class AbstractNamedEntity extends AbstractBaseEntity { + + protected String name; + + protected AbstractNamedEntity(Integer id, String name) { + super(id); + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public String toString() { + return String.format("Entity %s (%s, '%s')", getClass().getName(), id, name); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java new file mode 100644 index 000000000000..84d62071ad9c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -0,0 +1,6 @@ +package ru.javawebinar.topjava.model; + +public enum Role { + ROLE_USER, + ROLE_ADMIN +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java new file mode 100644 index 000000000000..d88e381945d8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -0,0 +1,91 @@ +package ru.javawebinar.topjava.model; + +import java.util.Date; +import java.util.EnumSet; +import java.util.Set; + +import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; + +public class User extends AbstractNamedEntity { + + private String email; + + private String password; + + private boolean enabled = true; + + private Date registered = new Date(); + + private Set roles; + + private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + + public User(Integer id, String name, String email, String password, Role role, Role... roles) { + this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, EnumSet.of(role, roles)); + } + + public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Set roles) { + super(id, name); + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + this.enabled = enabled; + this.roles = roles; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } + + public Date getRegistered() { + return registered; + } + + public void setRegistered(Date registered) { + this.registered = registered; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getCaloriesPerDay() { + return caloriesPerDay; + } + + public void setCaloriesPerDay(int caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public boolean isEnabled() { + return enabled; + } + + public Set getRoles() { + return roles; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return "User (" + + "id=" + id + + ", email=" + email + + ", name=" + name + + ", enabled=" + enabled + + ", roles=" + roles + + ", caloriesPerDay=" + caloriesPerDay + + ')'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java new file mode 100644 index 000000000000..c37b84d5fd77 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public interface UserRepository { + User save(User user); + + // false if not found + boolean delete(int id); + + // null if not found + User get(int id); + + // null if not found + User getByEmail(String email); + + List getAll(); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java new file mode 100644 index 000000000000..b63fc9b1df00 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -0,0 +1,4 @@ +package ru.javawebinar.topjava.service; + +public interface MealService { +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java new file mode 100644 index 000000000000..9017380f392b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java @@ -0,0 +1,9 @@ +package ru.javawebinar.topjava.service; + +import ru.javawebinar.topjava.repository.MealRepository; + +public class MealServiceImpl implements MealService { + + private MealRepository repository; + +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java new file mode 100644 index 000000000000..d0cf33e30815 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -0,0 +1,22 @@ +package ru.javawebinar.topjava.service; + + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.List; + +public interface UserService { + + User create(User user); + + void delete(int id) throws NotFoundException; + + User get(int id) throws NotFoundException; + + User getByEmail(String email) throws NotFoundException; + + void update(User user); + + List getAll(); +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java new file mode 100644 index 000000000000..d2f2a9103a0f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava.service; + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; + +public class UserServiceImpl implements UserService { + + private UserRepository repository; + + @Override + public User create(User user) { + return repository.save(user); + } + + @Override + public void delete(int id) throws NotFoundException { + checkNotFoundWithId(repository.delete(id), id); + } + + @Override + public User get(int id) throws NotFoundException { + return checkNotFoundWithId(repository.get(id), id); + } + + @Override + public User getByEmail(String email) throws NotFoundException { + return checkNotFound(repository.getByEmail(email), "email=" + email); + } + + @Override + public List getAll() { + return repository.getAll(); + } + + @Override + public void update(User user) { + repository.save(user); + } +} \ 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..2914ed684410 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava.util; + + +import ru.javawebinar.topjava.model.AbstractBaseEntity; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +public class ValidationUtil { + + public static void checkNotFoundWithId(boolean found, int id) { + checkNotFound(found, "id=" + id); + } + + public static T checkNotFoundWithId(T object, int id) { + return checkNotFound(object, "id=" + id); + } + + public static T checkNotFound(T object, String msg) { + checkNotFound(object != null, msg); + return object; + } + + public static void checkNotFound(boolean found, String msg) { + if (!found) { + throw new NotFoundException("Not found entity with " + msg); + } + } + + public static void checkNew(AbstractBaseEntity entity) { + if (!entity.isNew()) { + throw new IllegalArgumentException(entity + " must be new (id=null)"); + } + } + + public static void assureIdConsistent(AbstractBaseEntity entity, int id) { +// http://stackoverflow.com/a/32728226/548473 + if (entity.isNew()) { + entity.setId(id); + } else if (entity.getId() != id) { + throw new IllegalArgumentException(entity + " must be with id=" + id); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java new file mode 100644 index 000000000000..f1e9b0e46376 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java new file mode 100644 index 000000000000..ab4e8ea8bb8e --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -0,0 +1,8 @@ +package ru.javawebinar.topjava.web.meal; + +import ru.javawebinar.topjava.service.MealService; + +public class MealRestController { + private MealService service; + +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java new file mode 100644 index 000000000000..16a68259ec20 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -0,0 +1,49 @@ +package ru.javawebinar.topjava.web.user; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; + +import java.util.List; + +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; + +public abstract class AbstractUserController { + protected final Logger log = LoggerFactory.getLogger(getClass()); + + private UserService service; + + public List getAll() { + log.info("getAll"); + return service.getAll(); + } + + public User get(int id) { + log.info("get {}", id); + return service.get(id); + } + + public User create(User user) { + log.info("create {}", user); + checkNew(user); + return service.create(user); + } + + public void delete(int id) { + log.info("delete {}", id); + service.delete(id); + } + + public void update(User user, int id) { + log.info("update {} with id={}", user, id); + assureIdConsistent(user, id); + service.update(user); + } + + public User getByMail(String email) { + log.info("getByEmail {}", email); + return service.getByEmail(email); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java new file mode 100644 index 000000000000..ae3374468e41 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava.web.user; + +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +public class AdminRestController extends AbstractUserController { + + @Override + public List getAll() { + return super.getAll(); + } + + @Override + public User get(int id) { + return super.get(id); + } + + @Override + public User create(User user) { + return super.create(user); + } + + @Override + public void delete(int id) { + super.delete(id); + } + + @Override + public void update(User user, int id) { + super.update(user, id); + } + + @Override + public User getByMail(String email) { + return super.getByMail(email); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java new file mode 100644 index 000000000000..865f4c015d8d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava.web.user; + +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.model.User; + +public class ProfileRestController extends AbstractUserController { + + public User get() { + return super.get(AuthorizedUser.id()); + } + + public void delete() { + super.delete(AuthorizedUser.id()); + } + + public void update(User user) { + super.update(user, AuthorizedUser.id()); + } +} \ No newline at end of file From 1f1447f6f87554bf5728c9f792d826bf46b1e5b3 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 9 Nov 2017 21:15:04 +0300 Subject: [PATCH 017/139] 2_4 add spring context --- pom.xml | 15 +++++++ .../ru/javawebinar/topjava/SpringMain.java | 19 ++++++++ .../InMemoryMealRepositoryImpl.java | 3 +- .../mock/MockUserRepositoryImpl.java | 43 +++++++++++++++++++ .../javawebinar/topjava/web/MealServlet.java | 2 +- src/main/resources/spring/spring-app.xml | 7 +++ 6 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/SpringMain.java rename src/main/java/ru/javawebinar/topjava/repository/{ => mock}/InMemoryMealRepositoryImpl.java (90%) create mode 100644 src/main/java/ru/javawebinar/topjava/repository/mock/MockUserRepositoryImpl.java create mode 100644 src/main/resources/spring/spring-app.xml diff --git a/pom.xml b/pom.xml index 6f484d0beec9..537022542c3b 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,8 @@ UTF-8 UTF-8 + 4.3.12.RELEASE + 1.2.3 1.7.25 @@ -60,6 +62,19 @@ runtime + + + org.springframework + spring-context + ${spring.version} + + + commons-logging + commons-logging + + + + javax.servlet diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java new file mode 100644 index 000000000000..bb4a84fa88b4 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Arrays; + +public class SpringMain { + public static void main(String[] args) { + ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); + +// UserRepository userRepository = (UserRepository) appCtx.getBean("mockUserRepository"); + UserRepository userRepository = appCtx.getBean(UserRepository.class); + userRepository.getAll(); + appCtx.close(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/mock/InMemoryMealRepositoryImpl.java similarity index 90% rename from src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepositoryImpl.java rename to src/main/java/ru/javawebinar/topjava/repository/mock/InMemoryMealRepositoryImpl.java index bcb9cd8894a0..9eecc3b4c7cd 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/InMemoryMealRepositoryImpl.java +++ b/src/main/java/ru/javawebinar/topjava/repository/mock/InMemoryMealRepositoryImpl.java @@ -1,6 +1,7 @@ -package ru.javawebinar.topjava.repository; +package ru.javawebinar.topjava.repository.mock; import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.repository.MealRepository; import ru.javawebinar.topjava.util.MealsUtil; import java.util.Collection; diff --git a/src/main/java/ru/javawebinar/topjava/repository/mock/MockUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/mock/MockUserRepositoryImpl.java new file mode 100644 index 000000000000..0825c7b69afa --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/mock/MockUserRepositoryImpl.java @@ -0,0 +1,43 @@ +package ru.javawebinar.topjava.repository.mock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.Collections; +import java.util.List; + +public class MockUserRepositoryImpl implements UserRepository { + private static final Logger log = LoggerFactory.getLogger(MockUserRepositoryImpl.class); + + @Override + public boolean delete(int id) { + log.info("delete {}", id); + return true; + } + + @Override + public User save(User user) { + log.info("save {}", user); + return user; + } + + @Override + public User get(int id) { + log.info("get {}", id); + return null; + } + + @Override + public List getAll() { + log.info("getAll"); + return Collections.emptyList(); + } + + @Override + public User getByEmail(String email) { + log.info("getByEmail {}", email); + return null; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java index 065bb4ff8d81..0dca99ceb244 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MealServlet.java +++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java @@ -3,8 +3,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.javawebinar.topjava.model.Meal; -import ru.javawebinar.topjava.repository.InMemoryMealRepositoryImpl; import ru.javawebinar.topjava.repository.MealRepository; +import ru.javawebinar.topjava.repository.mock.InMemoryMealRepositoryImpl; import ru.javawebinar.topjava.util.MealsUtil; import javax.servlet.ServletConfig; diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml new file mode 100644 index 000000000000..e2e1f0314c0d --- /dev/null +++ b/src/main/resources/spring/spring-app.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file From 61c96a2f2650cf6153fd0e1f2a17e8fd5f03bb90 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 9 Nov 2017 21:15:30 +0300 Subject: [PATCH 018/139] 2_5 dependency injection --- src/main/java/ru/javawebinar/topjava/SpringMain.java | 7 +++++++ .../ru/javawebinar/topjava/service/UserServiceImpl.java | 4 ++++ src/main/resources/spring/spring-app.xml | 3 +++ 3 files changed, 14 insertions(+) diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java index bb4a84fa88b4..2da5cacd3228 100644 --- a/src/main/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -2,7 +2,10 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.service.UserService; import java.util.Arrays; @@ -14,6 +17,10 @@ public static void main(String[] args) { // UserRepository userRepository = (UserRepository) appCtx.getBean("mockUserRepository"); UserRepository userRepository = appCtx.getBean(UserRepository.class); userRepository.getAll(); + + UserService userService = appCtx.getBean(UserService.class); + userService.create(new User(null, "userName", "email", "password", Role.ROLE_ADMIN)); + appCtx.close(); } } diff --git a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java index d2f2a9103a0f..753c3cce04e5 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java @@ -13,6 +13,10 @@ public class UserServiceImpl implements UserService { private UserRepository repository; + public void setRepository(UserRepository repository) { + this.repository = repository; + } + @Override public User create(User user) { return repository.save(user); diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index e2e1f0314c0d..7b4fa1689425 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -4,4 +4,7 @@ + + + \ No newline at end of file From 13b57435050b5364813a8ef3d0923d8506797a77 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 9 Nov 2017 21:15:58 +0300 Subject: [PATCH 019/139] 2_6 annotation processing --- .../ru/javawebinar/topjava/SpringMain.java | 20 ++++++---------- .../mock/MockUserRepositoryImpl.java | 2 ++ .../topjava/service/UserServiceImpl.java | 6 +++++ .../web/user/AbstractUserController.java | 2 ++ .../topjava/web/user/AdminRestController.java | 2 ++ .../web/user/ProfileRestController.java | 2 ++ src/main/resources/spring/spring-app.xml | 23 +++++++++++++++---- 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/SpringMain.java b/src/main/java/ru/javawebinar/topjava/SpringMain.java index 2da5cacd3228..9fa857a7c46a 100644 --- a/src/main/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/main/java/ru/javawebinar/topjava/SpringMain.java @@ -4,23 +4,17 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; -import ru.javawebinar.topjava.repository.UserRepository; -import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.web.user.AdminRestController; import java.util.Arrays; public class SpringMain { public static void main(String[] args) { - ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml"); - System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); - -// UserRepository userRepository = (UserRepository) appCtx.getBean("mockUserRepository"); - UserRepository userRepository = appCtx.getBean(UserRepository.class); - userRepository.getAll(); - - UserService userService = appCtx.getBean(UserService.class); - userService.create(new User(null, "userName", "email", "password", Role.ROLE_ADMIN)); - - appCtx.close(); + // java 7 Automatic resource management + try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml")) { + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); + AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); + adminUserController.create(new User(null, "userName", "email", "password", Role.ROLE_ADMIN)); + } } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/mock/MockUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/mock/MockUserRepositoryImpl.java index 0825c7b69afa..3825d9a48654 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/mock/MockUserRepositoryImpl.java +++ b/src/main/java/ru/javawebinar/topjava/repository/mock/MockUserRepositoryImpl.java @@ -2,12 +2,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Repository; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import java.util.Collections; import java.util.List; +@Repository public class MockUserRepositoryImpl implements UserRepository { private static final Logger log = LoggerFactory.getLogger(MockUserRepositoryImpl.class); diff --git a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java index 753c3cce04e5..109a60fb7f36 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.service; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import ru.javawebinar.topjava.util.exception.NotFoundException; @@ -9,13 +11,17 @@ import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; +@Service public class UserServiceImpl implements UserService { + @Autowired private UserRepository repository; +/* public void setRepository(UserRepository repository) { this.repository = repository; } +*/ @Override public User create(User user) { diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index 16a68259ec20..0000f1c1e02f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; @@ -13,6 +14,7 @@ public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); + @Autowired private UserService service; public List getAll() { diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index ae3374468e41..b37a8ed6c8a5 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -1,9 +1,11 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.stereotype.Controller; import ru.javawebinar.topjava.model.User; import java.util.List; +@Controller public class AdminRestController extends AbstractUserController { @Override diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index 865f4c015d8d..eb4c0f3f3ada 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -1,8 +1,10 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.stereotype.Controller; import ru.javawebinar.topjava.AuthorizedUser; import ru.javawebinar.topjava.model.User; +@Controller public class ProfileRestController extends AbstractUserController { public User get() { diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 7b4fa1689425..96e40e902b66 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -1,10 +1,23 @@ + xmlns:context="http://www.springframework.org/schema/context" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> - + + + + + + + + + - - - \ No newline at end of file From 6952b6468bf48cf5f3b53f1e42ec646389fd92b5 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 9 Nov 2017 21:16:32 +0300 Subject: [PATCH 020/139] 2_7 constructor injection --- .../ru/javawebinar/topjava/service/UserServiceImpl.java | 8 +++----- src/main/resources/spring/spring-app.xml | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java index 109a60fb7f36..233d71e07f50 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java @@ -14,14 +14,12 @@ @Service public class UserServiceImpl implements UserService { - @Autowired - private UserRepository repository; + private final UserRepository repository; -/* - public void setRepository(UserRepository repository) { + @Autowired + public UserServiceImpl(UserRepository repository) { this.repository = repository; } -*/ @Override public User create(User user) { diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 96e40e902b66..538554fb7cbd 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -5,11 +5,11 @@ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> From 2358e01ad6cf70c031efb1d719a7f7e71c455ff3 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sat, 11 Nov 2017 20:17:04 +0300 Subject: [PATCH 021/139] add doc (lessons tasks) --- doc/lesson01.md | 1081 +++++++++++++++++++++++++++++++++++++++++++++++ doc/lesson02.md | 1030 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2111 insertions(+) create mode 100644 doc/lesson01.md create mode 100644 doc/lesson02.md diff --git a/doc/lesson01.md b/doc/lesson01.md new file mode 100644 index 000000000000..12579644dcd2 --- /dev/null +++ b/doc/lesson01.md @@ -0,0 +1,1081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + topjava/lesson01.md at doc · JavaWebinar/topjava + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Skip to content +
    + + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    + +
      +
    • +
      + +
      + + + +
      +
      +
      + + Notifications +
      + + + +
      +
      +
      +
      +
    • + +
    • + +
      +
      + + + +
      +
      + + + +
      + +
    • + +
    • +
      + +
      + +
    • +
    + +

    + + /topjava + +

    + +
    + + + + +
    + +
    +
    + + + Permalink + + + +
    + +
    + + +
    + +
    +
    + + Switch branches/tags +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    +
    + + +
    + +
    Nothing to show
    +
    + +
    +
    +
    + +
    + + Find file + + +
    + +
    + + + +
    + + + 01d16ad + + Nov 9, 2017 + + + +
    + + +
    + + +
    + + +
    +
    +
    + +
    + Raw + Blame + History +
    + + + + + +
    + +
    + +
    + +
    + 281 lines (211 sloc) + + 33.1 KB +
    +
    + + +
    +

    Онлайн проект Topjava

    +
      +
    • Не стоит стремиться прочитать все ссылки урока, их можно использовать как справочник. Гораздо важнее пройти основной материал урока и сделать Домашнее Задание
    • +
    • Обязательно посмотри правила работы с патчами на проекте
    • +
    • Делать Apply Patch лучше по одному, непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (Version Control->Local Changes-> Ctrl+D)
    • +
    • Код проекта обновляется и не всегда совпадает с видео (можно увидеть как развивался проект). Изменения в проекте указываю после соответствующего патча.
    • +
    +

    Материалы занятия (скачать все патчи можно через Download/Скачать папки patch)

    +

    image

    +

    correction Рефакторинг проекта

    +

    Apply 1_0_fix.patch

    +
    +
      +
    • переименовал классы UserMeal* в более красивые Meal*
    • +
    • удалил себя из шапки классов
    • +
    • обновил версию maven-compiler-plugin
    • +
    +
    +

    hw Разбор домашнего задания HW0:

    +

    video 1. Optional: реализация getFilteredMealsWithExceeded через Stream API

    +

    Apply 1_1_HW0_stream.patch

    + +

    video 2. Работа с git в IDEA. Реализация через цикл.

    +

    Apply 1_2_HW0_cycle.patch

    +
    +

    Изменения в проекте: map.getOrDefault заменил на map.merge, for заменены на forEach

    +
    +

    question Вопросы по HW0

    +
    +

    что делает метод Map.merge ?

    +
    +

    никогда не надо лениться зайти в код Map.merge (Ctrl+мышка на методе) и почитать javadoc (Ctrl+Q). Когда привыкать, если не сейчас?

    +
    +

    что означает Integer::sum ?

    +
    +

    это ссылка на метод, сокращенная форма лямбды. IDEA иногда выделяет желтым и предлагает замену (соглашаться по Alt+Enter). +Например m->m.getCalories() заменяется на Meal::getCalories.

    +
    +

    почему не использовать в TimeUtil методы isBefore/isAfter ?

    +
    +

    это строгие (excluded) сравнения, а нам также нужны краевые значения

    +
    +

    В MealsUtil у нас где-то есть ключевое слово final, где-то нет. В чем разница?

    +
    +

    Я участвовал в одном проекте, где final был обязательным (в сеттингах IDEA галочка стояла). Но это скорее исключение, чем правило в проектах java (в Java 8 вообще ввели эффективный final, те по факту). Во всех новомодных языках переменные final по умолчанию, а в java нужно помнить и везде добавлять, утомительно. Но если приучитесь - хуже не будет. Я обычно ставлю там, где важно по смыслу (если не забываю).

    +
    +

    Почему у User.registered тип Date, а Meal.dateTime LocalDateTime?

    +
    +

    Время регистрации - абсолютное (конкретный момент), а время еды по бизнес логике относительно (те не зависит от часового пояса)

    +

    video 3. HW0 Optional 2: реализация в один проход и одной строкой Stream API

    +

    Apply 1_3_HW0_optional2.patch

    +

    Занятие 1:

    +

    video 4. Обзор используемых в проекте технологий. Интеграция ПО.

    + +

    video 5. Maven.

    + +

    video 6. WAR. Веб-контейнер Tomcat. Сервлеты.

    +
    +
      +
    • Нам нужна 8-я версия Tomcat (например последняя 8.5.16)
    • +
    • Устанавливать Tomcat лучше простым скачиванием архива apache-tomcat-8.5.16[-windows-xxx].zip и копированием из него в каталог без пробелов и русских букв (пример С:\java\apache-tomcat-8.5.16). Не используйте Tomcat 8.5.13 и 9.0.0.M19 IDEA из-за бага Tomcat
    • +
    +
    +

    Apply 1_4_switch_to_war.patch

    +
    +
      +
    • Обновил сервлеты до версии 3.1 +(Tomcat 8 использует это API, хотя для нас не принципиально, т.к. мы никакие фичи 3.x Servlet API не используем)
    • +
    • Переименовал userList.jsp в users.jsp
    • +
    • Сервлет добавляется в следующем патче, те в web.xml он будет подчеркиваться красным.
    • +
    +
    +

    Apply 1_5_add_servlet_api.patch

    +
    + +
    +

    Apply 1_6_forward_to_redirect.patch

    + +
    <user username="tomcat" password="tomcat" roles="tomcat,manager-gui,admin-gui"/>
    +
    + +

    video 7. Логирование.

    +

    Apply 1_7_logging.patch

    +
    +

    Установите переменную окружения на TOPJAVA_ROOT на корень проекта и перезапустите IDEA. Слеши в пути должны быть в стиле unix (/)

    +
    +
    +
      +
    • изменения в проекте: убрал LoggerWrapper и логирую напрямую в логгер SLF4J. При логгировании через вспомогательный класс, в логе теряется имя исходного класса.
    • +
    • удалил зависимость jul-to-slf4j. Она нам не нужна и, согласно видео Владимира Красильщика про логирование, она замедляет работу
    • +
    • Не делать конкатенацию строк при логгировании сообщений, если уровень логирования в конфигурации выставлен выше уровня логирования в коде + +
    • +
    +
    + +

    Проверочные вопросы:

    +
      +
    • Что нужно изменить в pom.xml, чтобы перейти с logback на log4j ?
    • +
    +

    question Ваши вопросы

    +
    +

    Почему private static final Logger log а не LOG ?

    +
    + +
    +

    Используются ли еще где-то в реальной разработке JSP, или это уже устаревшая технология? Заменит ли ее JSF (https://javatalks.ru/topics/38037)?

    +
    +

    JSF и JSP- разные ниши и задачи. +JSP- шаблонизатор, JSF- МVС фреймворк. Из моего опыта- с JSP сталкивался в 60% проектов. Его прямая замена: http://www.thymeleaf.org (в Spring-Boot по умолчанию), но в уже запущенных проектах встречается достаточно редко. JSP не умирает, потому что просто и дешево. Кроме того включается в большинство веб-контейнеров (в Tomcat его реализация Jasper)

    +

    JSF- sun-овский еще фреймворк, с которым я ни разу не сталкивался и особого желания нет. Вот он как раз, по моему мнению, активно замещается хотя бы javascript фреймворками (angular, ember, react).

    +
    +

    А зачем мы использовали logback? Почему SLF4J нас не устроило? Почему реализация логирования не log4j?

    +
    +

    SLF4J-API это API. Там есть только пустая реализация org.slf4j.helpers.NOPLogger (можно посмотреть в исходниках). Logback для новых проектов стал стандарт. spring-petclinic и spring-boot используют его по умолчанию.

    + +
    +

    hw Домашнее задание HW01 (делаем ветку HW01 от последнего патча в master)

    +
    +

    На проекте мы учимся мыслить и работать как Java разработчики уже сейчас, потом это будет гораздо сложнее и стоить дороже. +Вот на мой взгляд хорошие советы новичкам. От себя я добавлю:

    +
      +
    • Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В процессе формулирования очень часто приходит ее решение.
    • +
    • Учись исследовать проблему. Внимательное чтение логов и умение дебажить - основные навыки разработчика. Обычно самый верх самого нижнего эксепшена- причина ошибки, туда нужно ставить брекпойнт.
    • +
    • Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и биться над ней часами. Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время.
    • +
    +
    +

    1. Реализовать сервлет с отображением в таблице списка еды (в памяти и БЕЗ учета пользователя)

    +
    +

    Деплоиться в Tomcat лучше как war exploded: нет упаковки в war и при нажатой кнопке Update Resources on Frame Deactivation можно обновляться css, html, jsp без передеплоя. При изменении web.xml, добавлении методов, классов необходим redeploy.

    +
    +
      +
    • 1.1 По аналогии с UserServlet добавить MealServlet и meals.jsp +
        +
      • Задеплоить приложение (war) в Tomcat c applicationContext=topjava (приложение должно быть доступно по http://localhost:8080/topjava)
      • +
      • Попробовать разные деплои в Tomcat, remote и local debug
      • +
      +
    • +
    • 1.2 Сделать отображения списка еды в JSP, цвет записи в таблице зависит от параметра exceed (красный/зеленый). +
        +
      • 1.2.1 Список еды захардкодить (те проинициализировать в коде, желательно чтобы в проекте инициализация была только в одном месте)
      • +
      • 1.2.2 Время выводить без 'T'
      • +
      • 1.2.3 Список выводим БЕЗ фильтрации по startTime/endTime
      • +
      • 1.2.4 Вариант реализации: +
          +
        • из сервлета преобразуете еду в List<MealWithExceeded>;
        • +
        • кладете список в запрос (request.setAttribute);
        • +
        • делаете forward на jsp для отрисовки таблицы (при redirect атрибуты теряются).
        • +
        • в JSP для цикла можно использовать JSTL tag forEach. Для подключения JSTL в pom.xml и шапку JSP нужно добавить:
        • +
        +
      • +
      +
    • +
    +
        <dependency>
    +       <groupId>javax.servlet</groupId>
    +       <artifactId>jstl</artifactId>
    +       <version>1.2</version>
    +    </dependency>
    +
    +    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    +    ...
    +
    + +

    Optional

    +

    2. Реализуем в ПАМЯТИ CRUD (create/read/update/delete) для еды

    +

    Пример: Simple CRUD using Servlet/JSP

    +
      +
    • 2.1 Хранение в памяти будет одна из наших CRUD реализаций (позже будет JDBC, JPA и DATA-JPA).
    • +
    • 2.2 Работать с реализацией CRUD через интерфейс, который не должен ничего знать о деталях реализации (Map, DB или что-то еще).
    • +
    • 2.3 Добавить поле id в Meal/ MealWithExceed и реализовать генерацию счетчика, УЧЕСТЬ МНОГОПОТОЧНОСТЬ СЕРВЛЕТОВ + +
    • +
    • 2.4 Сделать форму редактирования в JSP: AJAX/JavaScript использовать НЕ надо, делаем через <form method="post"> и doPost() в сервлете.
    • +
    • 2.5 Для ввода дат и времени можно использовать html5 типы, хотя они поддерживаются не всеми браузерами (протестировать свой браузер). В конце курса мы добавим DateTimePicker jQuery plugin, который будет работать на всех браузерах.
    • +
    +

    После выполнения ДЗ обязательно проверьте решение на ошибки

    +

    question Вопросы по HW1

    +
    +

    Не попадаю на страничку/брекпойнт в сервлете.

    +
    +
      +
    • внимательно проверь url и applicationContext (Application Context должен быть тот же, что и url приложения: wiki IDEA)
    • +
    • посмотрите в task manager: возможно запущено несколько JVM и они мешают друг другу. Лишние java приложения убить.
    • +
    +
    +

    Приложение не видит TOPJAVA_ROOT.

    +
    +

    После выставления переменной окружения IDEA нужно рестартовать. Слеши в пути должны быть в стиле unix (/). Проверить, видит ли java переменную окружения можно так: System.getenv("TOPJAVA_ROOT"). Еще вариант: добавить -DTOPJAVA_ROOT=... в опции запуска приложения, тогда она доступна из java как System.getProperty("TOPJAVA_ROOT").

    +
    +

    Проблемы с кодировкой в POST (кракозябры).

    +
    +

    Возможное решение - выставьте кодировку ДО первого чтения из request:

    +
    protected void doPost(HttpServletRequest request, ...) {
    +    request.setCharacterEncoding("UTF-8");
    +
    +
    +

    Если сервлет тыкают несколько пользователей / несколько браузеров, какого должно быть поведение? Нужно ли что-то делать с сессиями?

    +
    +

    В Optional нужно делать реализацию хранения многопоточной. Cессии пока не используем (начнутся, когда будет прикручивать авторизацию).

    +
    +

    Для чего нам нужна многопоточная реализация коллекции, если каждый пользователь видит только себя?

    +
    +

    Реализация хранения в памяти у нас одна на всех. Те коллекция шарится между пользователями, а они в разных потоках ее дергают. Если несколько потоков одновременно будут изменять коллекцию без учета многопоточности (например один будет удалять, второй вставлять), мы получим ConcurrentModificationException

    +
    +

    Предпочтительнее ли создавать новый объект Meal при каждом update?

    +
    +

    Если при обновлении не создавать копию, то сохраненный в памяти объект может кто-то попортить. Вопрос скорее доверия к коду- если проект большой и людей над ним трудится много, то вероятнее нужно копировать.

    +
    +

    Почему теряются атрибуты при передаче на сервлет: http://localhost:8080/topjava/meals?action=add&... и req.getAttribute("action") = null ?

    +
    +

    См. Difference between getAttribute() and getParameter(). Отсюда также следует, что при редиректе атрибуты теряются.

    +
    +

    Зачем нужен в jsp <jsp:useBean id=".." scope="request" type=".."/> ?

    +
    +

    jsp:useBean нужен IDEA для автодополнений - она понимает тип переменной, которая уже доступна в JSP (например через setAttribute). И еще эта переменная становится доступной в java вставках. Для вывода в JSP это тэг не обязателен. Если тип переменной JSP не совпадает с тем, что в jsp:useBean, будет ошибка.

    +
    +

    error Типичные ошибки

    +
      +
    • 1 Если в названии класса есть Meal, не нужно использовать слово meal в методах класса.
    • +
    • 2 Привыкайте писать комментарии к чекину: одной фразой что вы сделали в нем. Например: Meals CRUD implementation. См. +Как писать сообщения коммитов
    • +
    • 3 Хранение в памяти и операции с ней должны выполняться просто и эффективно
    • +
    • 4 Хранить нужно Meal и конвертировать ее в MealWithExceed когда отдаем список на отображение в JSP. Иначе при редактировании любой записи или изменении юзером своей нормы caloriesPerDay нужно будет пересчитывать все записи юзера.
    • +
    • 5 Стили color можно применять ко всей строке таблицы tr, а не каждой ячейке.
    • +
    • 6 DateTimeFormatter можно сделать один заранее (он потокобезопасный в отличие от SimpleDateFormatter), а не создавать новый при каждом запросе.
    • +
    • 7 Работать с CRUD надо через интерфейс.
    • +
    • 8 Реализаций хранения будет несколько, нужно учитывать это в названии класса имплементации работы в памяти.
    • +
    • 9 В web.xml принято группировать сервлет со своим маппингом
    • +
    • 10 Не размещайте никакую логику (форматирование, счетчики) в бинах, где хранятся только данные (Meal, MealWithExceed)
    • +
    • 11 Еще раз: детали реализации в памяти не должны быть никому видны. Те НЕ НАДО счетчик размещать в Meal или MealServlet или MealsUtil, в базе же он будет по другому генерится.
    • +
    • 12 volatile при ++ не помогает от многопоточности. Почему?
    • +
    • 13 Обратите также внимание на то, чтобы реализация вашей коллекции для хранения еды была также многопоточной.
    • +
    • 14 Не делайте дублирование кода MealsUtil. Возможно вам пригодятся константы LocalTime.MIN и LocalTime.MAX
    • +
    • 15 Не дублируйте строки в jsp. Посмотрите на тернарный оператор.
    • +
    • 16 после операции delete в браузере должен быть url http:\\localhost:8080\topjava\meals
    • +
    • 17 Перед чекином проверяйте свой ченджлист (Ctrl+D на файле из Local Changes - посмотреть что поменялось). Если там только пробелы/переводы строк, не надо его комитить - делайте файлу Git->revert.
    • +
    • 18 Учтите в названии реализации CRUD, что +
        +
      • 18.1 у нас будет несколько реализаций (не только в памяти)
      • +
      • 18.2 у нас будет 2 CRUD (для еды и пользователей). А в реальном проекте их намного больше.
      • +
      +
    • +
    • 19 Сессии НЕ использовать! При редиректе все атрибуты (req.getAttribute()) теряются (см. вопрос выше). Сценарий редиректа: +
        +
      • 1 из сервлета делаем редирект (снова на сервлет, не на JSP!)
      • +
      • 2 снова заходим в сервлет
      • +
      • 3 кладем нужные атрибуты и делаем forward на jsp
      • +
      • 4 если очень хочется передать параметры из 1. в 2. можно сделать их через параметры запроса (например meals?id=5) и доставать через reg.getParameter(id). В моей реализации такого не потребовалось.
      • +
      +
    • +
    +
    +
    + +
    + + + + +
    + +
    + +
    +
    + +
    + + + + + + +
    + + + You can't perform that action at this time. +
    + + + + + + + + + + +
    + + You signed in with another tab or window. Reload to refresh your session. + You signed out in another tab or window. Reload to refresh your session. +
    + + + + + + diff --git a/doc/lesson02.md b/doc/lesson02.md new file mode 100644 index 000000000000..cf1a76ae67a2 --- /dev/null +++ b/doc/lesson02.md @@ -0,0 +1,1030 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + topjava/lesson02.md at doc · JavaWebinar/topjava + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Skip to content +
    + + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +
    +
    + + + + + + +
    +
    + +
      +
    • +
      + +
      + + + +
      +
      +
      + + Notifications +
      + + + +
      +
      +
      +
      +
    • + +
    • + +
      +
      + + + +
      +
      + + + +
      + +
    • + +
    • +
      + +
      + +
    • +
    + +

    + + /topjava + +

    + +
    + + + + +
    + +
    +
    + + + Permalink + + + +
    + +
    + + +
    + +
    +
    + + Switch branches/tags +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    +
    + + +
    + +
    Nothing to show
    +
    + +
    +
    +
    + +
    + + Find file + + +
    + +
    + + + +
    + + + e222e2c + + Nov 10, 2017 + + + +
    + + +
    + + +
    + + +
    +
    +
    + +
    + Raw + Blame + History +
    + + + + + +
    + +
    + +
    + +
    + 236 lines (176 sloc) + + 29 KB +
    +
    + + +
    +

    Онлайн проект Topjava

    +

    Материалы занятия (скачать все патчи можно через Download папки patch)

    +

    correction Правки в проекте

    +

    Apply 2_0_add_fix.patch

    +

    hw Разбор домашнего задания HW1:

    +

    Перед сборкой проекта (или запуском Tomcat)

    +
      +
    • откройте вкладку Maven Projects и сделайте clean
    • +
    • если страничка в браузере работает неверно, очистите кэш (Ctrl+F5 в хроме)
    • +
    +

    video 1. Отображения списка еды в JSP

    +

    Apply 2_1_HW1.patch

    +
    +
      +
    • Изменения в MealsUtil: +
        +
      • Сделал константу List<Meal> MEALS
      • +
      • Сделал вспомогательный метод getWithExceeded
      • +
      +
    • +
    • Форматирование даты сделал на основе JSTL LocalDateTime format
    • +
    • Переименовал TimeUtil в DateTimeUtil
    • +
    • Переименовал mealList.jsp в meals.jsp
    • +
    • Добавил еще один способы вывести dateTime через стандартную JSTL функцию replace (префикс fn в шапке также надо поменять)
    • +
    +
    + +

    video 2. Optional: реализация CRUD

    +

    Apply 2_2_HW1_optional.patch

    +

    Про использование паттерна Repository будет подробно рассказано в видео "Слои приложения"

    +
    +
      +
    • Переименовал mealEdit.jsp в mealForm.jsp
    • +
    • В meal.jsp используется параметр запроса param.action, который не кладется в атрибуты.
    • +
    • В MealServlet.doGet() сделал выбор через switch
    • +
    • Пофиксил багу c history.back() в mealForm.jsp для FireFox (коммит формы при Cancel, сделал type="button").
    • +
    +
    +

    Дополнительно:

    + +

    question Вопросы по HW1

    +
    +

    Зачем в InMemoryMealRepositoryImpl наполнять map с помощью нестатического блока инициализации, а не в конструкторе?

    +
    +

    Разницы нет. Сделал чтобы напомнить вам про эту конструкцию. Малоизвестные особенности Java

    +
    +

    Почему InMemoryMealRepositoryImpl не singleton?

    +
    +

    Начиная с Servlet API 2.3 пул сервлетов не создается, создается только один инстанс сервлетов. Те. InMemoryMealRepositoryImpl в нашем случае создается тоже только один раз. Далее все наши классы слоев приложения будут создаваться через Spring, бины которого по умолчанию являются синглтонами (в его контексте).

    +
    +

    Objects.requireNonNull в MealServlet.getId(request) если у нас нет id в запросе бросает NPE (NullPointerException). Но оно вылетит и без этого метода. Зачем он нужен и почему мы его не обрабатываем?

    +
    +

    Objects.requireNonNull - это проверка предусловия (будет подробно на 4-м занятии). Означает что в метод пришел неверный аргумент (должен быть не null) и приложение сообщает об ошибке сразу на входе (а не "может быть где-то потом"). См. What is the purpose of Objects#requireNonNull. Если ее проглатывать или замазывать, то приложение возможно где-то работает неверно (приходят неверные аргументы), а мы об этом не узнаем. Красиво обрабатывать ошибки будем на последних занятиях (Spring Exception Handling).

    +

    Занятие 2:

    +

    video 3. Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava

    + +

    video 4. Слои приложения. Создание каркаса приложения.

    +

    Apply 2_3_app_layers.patch

    +
    +
      +
    • Переименовал ExceptionUtil в ValidationUtil
    • +
    • Поменял LoggedUser на AuthorizedUser
    • +
    • Добавил проверку id пользователя, пришедшего в контроллер (treat IDs in REST body, "If it is a public API you should be conservative when you reply, but accept liberally")
    • +
    +
    +

    Слои приложения

    + +

    question Ваши вопросы

    +
    +

    Какова цель деления приложения на слои?

    +
    +

    Управляемость проекта (особенно большого) повышается на порядок:

    +
      +
    • Обеспечивается меньшая связываемость. Допустим если мы меняем что-то в контроллере, то сервис эти изменения не задевают.
    • +
    • Облегчается тестирование (мы будем тестировать слои сервисов и контроллеров отдельно)
    • +
    +
    +

    DTO используются когда есть избыточность запросов, которую мы уменьшаем, собрав данные из разных бинов в один? Когда DTO необходимо использовать?

    +
    +

    (D)TO может быть как частью одного entity (набор полей) так и набором нескольких entities. +В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например MealsWithExceeded мы отдаем наружу и он является Transfer Object, его надо пернести в пакет to. +На многих проектах (и собеседованиях) практикуют разделение на уровне maven модулей entity слоя от логики и соответствующей конвертацией ВСЕХ Entity в TO, даже если у них те же самые поля. +Хороший ответ когда TO обязательны есть на stackoverflow: When to Use.

    +
    +

    Почему контроллеры положили в папку web, а не в conrollers?

    +
    +

    Тоже самое что domain/model - просто разные названия.

    +
    +

    Зачем мы наследуем NotFoundException от RuntimeException?

    +
    +

    Так с ним удобнее работать. И у нас нет никаких действий по восстановлению состояния приложения (no recoverable conditions): checked vs unchecked exception. По последним данным checked exception вообще depricated: Ignore Checked Exceptions

    +
    +

    Зачем в API пишем NotFoundException, если они RuntimeException?

    +
    +

    Обычно не пишут. Я написал для информации разработчикам - здесь делаем проверку и может быть брошено.

    +
    +

    Зачем в AdminRestController переопределяются методы родителя с вызовом тех же родительских?

    +
    +

    Сделано на будущее, мы будем работать с AdminRestController.

    +
    +

    И что такое ProfileRestController?

    +
    +

    Контроллер, где авторизованный пользователь будет работать со своими данными

    +
    +

    Что лучше возвращать из API: Collection или List

    +
    +

    Вообще как правило возвращают List, если не просится по коду более общий случай (например возможный Set или Collection, возвращаемый Map.values()). Если возвращается отсортированный список, то List будет адекватнее.

    +

    video 5. Обзор Spring Framework. Spring Context.

    +

    Apply 2_4_add_spring_context.patch

    + +

    Apply 2_5_add_dependency_injection.patch

    + +

    Apply 2_6_add_annotation_processing.patch

    + +

    Apply 2_7_constructor_injection.patch

    + +

    В контроллерах Constructor Injection делать не стал, добавляется лишний код (попробуйте сделать сами). На каждом проекте свои правила, универсальных нет.

    +

    Дополнительно видео по Spring

    + +

    video 6. Пояснения к HW2. Обработка Autowired

    +

    <context:annotation-config/> говорит спрингу при поднятии контекста обрабатывать @Autowired (добавляется в контекст спринга AutowiredAnnotationBeanPostProcessor). После того, как все бины уже в контексте пост-процессор через отражение инжектит все @Autowired зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке.

    +

    question Ваши вопросы

    +
    +

    Что такое схема в spring-app.xml xsi:schemaLocation и зачем она нужна

    +
    +

    XML схема нужна для валидации xml, IDEA делает по ней атвозаполнение.

    +
    +

    Что означает для Spring

    +
    +
     <bean class="ru.javawebinar.topjava.service.UserServiceImpl">
    +     <property name="repository" ref="mockUserRepository"/>
    + </bean> ?
    +
    +

    Можно сказать так: создай и занеси в свой контекст экземпляр класса (бин) UserServiceImpl и заинжекть в его проперти из своего контекста бин mockUserRepository.

    +
    +

    Как биндинг происходит для @Autowired? Как поступать, если у нас больше одной реализации UserRepository?

    +
    +

    @Autowired инжектит по типу (т.е. ижектит класс который реализует UserRepository). Обычно он один. Если у нас несколько реализаций, Spring не поднимится и поругается - No unique bean. +В этом случае можно уточнить имя бина через @Qualifier. @Qualifier обычно добавляют только в случае нескольких реализаций.

    +
    +

    Почему нельзя сервлет помещать в Spring контекст?

    +
    +

    Сервлеты- это исключительно классы servlet-api (веб контейнера) и должны инстанциироваться и работать в нем. Те технически можно ( без init/destroy), но идеологически - неверно. Также НЕ надо работать с cервлетом из SpringMain.

    +
    +

    hw Домашнее задание HW02

    +
      +
    • 1: переименовать MockUserRepositoryImpl в InMemoryUserRepositoryImpl и имплементировать по аналогии с InMemoryMealRepositoryImpl (список пользователей возвращать отсортированным по имени)
    • +
    • 2: сделать Meal extends AbstractBaseEntity, MealWithExceed перенести в пакет ru.javawebinar.topjava.to (transfer objects)
    • +
    • 3: изменить MealRepository/InMemoryMealRepositoryImpl: в одном общем хранилище хранится вся еда всех пользователей. Любой пользователь авторизуется и видит/модифицирует только свою еду. +
        +
      • 3.1: если по запрошенному id еда отсутствует или чужая, возвращать null/false (см. комментарии в UserRepository)
      • +
      • 3.2: реализовать хранение еды для каждого пользователя можно с добавлением поля userId в Meal ИЛИ без него (как нравится). Напомню, что репозиторий один и приложение может работать одновременно с многими пользователями.
      • +
      • 3.3: список еды возвращать отсортированным по времени, последние записи наверху
      • +
      • 3.4: атомарность операций не требуется (коллизии при одновременном изменении одного пользователя можно не учитывать)
      • +
      +
    • +
    • 4: Реализовать слои приложения для функциональности "еда". API контроллера должна удовлетворять все потребности демо приложения и ничего лишнего (см. демо). +
        +
      • Смотрите на реализацию слоя для user и делаете по аналогии! Если там что-то непонятно, не надо исправлять или делать по своему. Задавайте вопросы. Если действительно нужна правка- я сделаю и напишу всем.
      • +
      • 4.1: после авторизации (сделаем позднее), id авторизованного юзера будет попадать в AuthorizedUser.id(). Запрос попадает в контроллер, методы которого будут доступны снаружи по http, т.е. запрос можно будет сделать с ЛЮБЫМ id для еды +(не принадлежащем авторизированному пользователю). Нельзя позволять модифицировать/смотреть чужую еду.
      • +
      • 4.2: AuthorizedUser известен только на слое web (см. реализацию ProfileRestController). MealService можно тестировать без подмены логики авторизации, принимаем в методах сервиса и репозитория параметр userId: id владельца еды.
      • +
      • 4.3: если еда не принадлежит авторизированному пользователю или отсутствует, в MealServiceImpl бросать NotFoundException.
      • +
      • 4.4: конвертацию в MealWithExceeded можно делать как в слое web, так и в service (Mapping Entity->DTO: Controller or Service?)
      • +
      • 4.5: в MealServiceImpl постараться сделать в каждом методе только одни запрос к MealRepository
      • +
      • 4.6 еще раз: не надо в названиях методов повторять названия класса (Meal).
      • +
      +
    • +
    • 5: включить классы еды в контекст Spring (добавить аннотации) и вызвать из SpringMain любой метод MealRestController (проверить что Spring все корректно заинжектил)
    • +
    +

    Optional

    +
      +
    • 6: в MealServlet сделать инициализацию Spring, достать MealRestController из контекста и работать с едой через него (как в SpringMain). pom.xml НЕ менять, работаем со spring-context. Сервлет обращается к контролеру, контроллер вызывает сервис, сервис - репозиторий. +
        +
      • 6.1: учесть, что когда будем работать через Spring MVC, MealServlet удалим, те вся логика должна быть в контроллере
      • +
      +
    • +
    • 7: добавить в meals.jsp и MealServlet две отдельные фильтрации еды: по дате и по времени (см. демо)
    • +
    • 8: добавить выбор текущего залогиненного пользователя (имитация авторизации, сделать Select с двумя элементами со значениями 1 и 2 в index.html и AuthorizedUser.setId(userId) в UserServlet). +Настоящая атворизация будет через Spring Security позже.
    • +
    +
    +

    error Подсказки по HW02 (для проверки, сначала сделайте самостоятельно!)

    +
      +
    • 1: В реализации InMemoryUserRepositoryImpl +
        +
      • 1.1: getByEmail попробуйте сделать через stream
      • +
      • 1.2: delete попробуйте сделать за одно обращение к map (без containsKey)
      • +
      • 1.3: предусмотрите случай одинаковых User.name (порядок должен быть зафиксированным).
      • +
      +
    • +
    • 2: В реализации InMemoryMealRepositoryImpl +
        +
      • 2.1: В Meal, которая приходит в контроллер нет никакой информации о пользователе (еда приходит в контроллер БЕЗ user/userId). Она может быть только авторизованного пользователя, поэтому что-то сравнивать с ним никакого смысла нет. По id еды и авторизованному пользователю нужно проверить ее принадлежность.
      • +
      • 2.2: get\update\delete - следите, чтобы не было NPE (NullPointException может быть, если в хранилище отсутствует узер или еда).
      • +
      • 2.3: Проверьте сценарий: авторизованный пользователь пробует изменить чужую еду (id еды ему не принадлежит).
      • +
      • 2.4: Фильтрацию по датам сделать в репозитории т.к. из базы будем брать сразу отфильтрованные по дням записи. Следите чтобы первый и последний день не были обрезаны, иначе сумма калорий будет неверная.
      • +
      • 2.5: Если запрашивается список и он пустой, не возвращайте NULL! По пустому списку можно легко итерироваться без риска NullPoinException.
      • +
      • 2.6: Не дублируйте код в getAll и метод с фильтрацией
      • +
      • 2.7: попробуйте учесть, что следующая реализация (сотрировка, фильтрация) будет делаться прямо в базе данных
      • +
      +
    • +
    • 3: Проверьте, что удалили Meal.id, он уже есть в базовом BaseEntity
    • +
    • 4: Проверку isBetweenDate сделать в DateTimeUtil. Попробуйте использовать дженерики и объеденить ее с isBetweenTime
    • +
    • 5: Реализация 'MealRestController' должен уметь обрабатывать запросы: +
        +
      • 5.1: Отдать свою еду (для отображения в таблице, формат List<MealWithExceed>), запрос БЕЗ параметров
      • +
      • 5.2: Отдать свою еду, отфильтрованную по startDate, startTime, endDate, endTime
      • +
      • 5.3: Отдать/удалить свою еду по id, параметр запроса - id еды. Если еда с этим id чужая или отсутствует - NotFoundException
      • +
      • 5.4: Сохранить/обновить еду, параметр запроса - Meal. Если обновляемая еда с этим id чужая или отсутствует - NotFoundException
      • +
      • 5.5: Сервлет мы удалим, а контроллер останется, поэтому возвращать List<MealWithExceed> надо из контроллера. И userId принимать в контроллере НЕЛЬЗЯ (иначе - для чего аторизация?). Подмену MIX/MAX для Date/Time также сделайте здесь.
      • +
      • 5.6: В REST при update принято передавать id (см. AdminRestController.update)
      • +
      • 5.7: Сделайте отдельный getAll без применения фильтра
      • +
      +
    • +
    • 6: Проверьте корректную обработку пустых значений date и time в контроллере
    • +
    • 7: Авторизированного пользователя берем из AuthorizedUser.id(), cм. ProfileRestController
    • +
    • 8: В MealServlet +
        +
      • 8.1: Закрывать springContext в сервлете грамотнее всего в HttpServlet.destroy(): если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглетон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно.
      • +
      • 8.2: Не храните параметры фильтра как члены класса сервлета, это не многопоточно! Один экземпляр сервлета используется всеми запросами на сервер, попробуйте дернуть его из 2х браузеров.
      • +
      +
    • +
    +
    +
    + +
    + + + + +
    + +
    + +
    +
    + +
    + + + + + + +
    + + + You can't perform that action at this time. +
    + + + + + + + + + + +
    + + You signed in with another tab or window. Reload to refresh your session. + You signed out in another tab or window. Reload to refresh your session. +
    + + + + + + From bfd86f3405c7b241514f032a20523fec1ba35d34 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Thu, 16 Nov 2017 21:09:47 +0300 Subject: [PATCH 022/139] HW02 add doc --- doc/lesson02.md | 87 ++-- doc/lesson03.md | 1054 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1100 insertions(+), 41 deletions(-) create mode 100644 doc/lesson03.md diff --git a/doc/lesson02.md b/doc/lesson02.md index cf1a76ae67a2..0f394f625e2f 100644 --- a/doc/lesson02.md +++ b/doc/lesson02.md @@ -18,8 +18,8 @@ - - + + @@ -36,10 +36,10 @@ - + - + @@ -48,7 +48,7 @@ - + @@ -63,13 +63,13 @@ - + - + @@ -253,7 +253,7 @@ Settings -
  • +
  • @@ -264,7 +264,7 @@ -
    +
    @@ -299,7 +299,7 @@
    @@ -494,9 +494,9 @@
    - Permalink + Permalink - +
    @@ -593,15 +593,15 @@
    - - e222e2c + + ca3278f - Nov 10, 2017 + Nov 16, 2017
    @@ -641,12 +641,12 @@ -
    +
    -
    +
    - 236 lines (176 sloc) + 239 lines (179 sloc) - 29 KB + 30.1 KB
    @@ -685,7 +685,7 @@
  • Форматирование даты сделал на основе JSTL LocalDateTime format
  • Переименовал TimeUtil в DateTimeUtil
  • Переименовал mealList.jsp в meals.jsp
  • -
  • Добавил еще один способы вывести dateTime через стандартную JSTL функцию replace (префикс fn в шапке также надо поменять)
  • +
  • Добавил еще один способ вывести dateTime через стандартную JSTL функцию replace (префикс fn в шапке также надо поменять)
    • @@ -766,13 +766,13 @@

      DTO используются когда есть избыточность запросов, которую мы уменьшаем, собрав данные из разных бинов в один? Когда DTO необходимо использовать?

      (D)TO может быть как частью одного entity (набор полей) так и набором нескольких entities. -В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например MealsWithExceeded мы отдаем наружу и он является Transfer Object, его надо пернести в пакет to. +В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например MealsWithExceeded мы отдаем наружу и он является Transfer Object, его надо перенести в пакет to. На многих проектах (и собеседованиях) практикуют разделение на уровне maven модулей entity слоя от логики и соответствующей конвертацией ВСЕХ Entity в TO, даже если у них те же самые поля. Хороший ответ когда TO обязательны есть на stackoverflow: When to Use.

      Почему контроллеры положили в папку web, а не в conrollers?

      -

      Тоже самое что domain/model - просто разные названия.

      +

      То же самое что domain/model - просто разные названия.

      Зачем мы наследуем NotFoundException от RuntimeException?

      @@ -792,13 +792,13 @@

      Что лучше возвращать из API: Collection или List

      -

      Вообще как правило возвращают List, если не просится по коду более общий случай (например возможный Set или Collection, возвращаемый Map.values()). Если возвращается отсортированный список, то List будет адекватнее.

      +

      Вообще, как правило, возвращают List, если не просится по коду более общий случай (например возможный Set или Collection, возвращаемый Map.values()). Если возвращается отсортированный список, то List будет адекватнее.

      video 5. Обзор Spring Framework. Spring Context.

      Apply 2_4_add_spring_context.patch

      Apply 2_5_add_dependency_injection.patch

      video 6. Пояснения к HW2. Обработка Autowired

      -

      <context:annotation-config/> говорит спрингу при поднятии контекста обрабатывать @Autowired (добавляется в контекст спринга AutowiredAnnotationBeanPostProcessor). После того, как все бины уже в контексте пост-процессор через отражение инжектит все @Autowired зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке.

      +

      <context:annotation-config/> говорит спрингу при поднятии контекста обрабатывать @Autowired (добавляется в контекст спринга AutowiredAnnotationBeanPostProcessor). После того, как все бины уже в контексте постпроцессор через отражение инжектит все @Autowired зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке.

      question Ваши вопросы

      Что такое схема в spring-app.xml xsi:schemaLocation и зачем она нужна

      -

      XML схема нужна для валидации xml, IDEA делает по ней атвозаполнение.

      +

      XML схема нужна для валидации xml, IDEA делает по ней автозаполнение.

      Что означает для Spring

      @@ -862,6 +863,10 @@ and beans

      Сервлеты- это исключительно классы servlet-api (веб контейнера) и должны инстанциироваться и работать в нем. Те технически можно ( без init/destroy), но идеологически - неверно. Также НЕ надо работать с cервлетом из SpringMain.


      +
        +
      • Еще раз смотрим на демо приложение и вникаем, что такое пользователь и его еда и что он может с ней сделать. Когда пользователь авторизуется в приложении, его id и норма калорий чудесным образом попадают в AuthorizedUser и в приложении мы может обращаемся к ним. Как они реально туда попадут будет в уроке 9 (Spring Security, сессия и куки)
      • +
      • Перед началом выполнения ДЗ (ели есть хоть какие-то сомнения) прочитайте ВСЕ ДЗ. Если вопросы остаются- то ВСЕ подсказки. Особенно этот пункт важный, когда будете делать реальное рабочее ТЗ.
      • +

      hw Домашнее задание HW02

      • 1: переименовать MockUserRepositoryImpl в InMemoryUserRepositoryImpl и имплементировать по аналогии с InMemoryMealRepositoryImpl (список пользователей возвращать отсортированным по имени)
      • @@ -912,16 +917,16 @@ and beans
      • 2: В реализации InMemoryMealRepositoryImpl
        • 2.1: В Meal, которая приходит в контроллер нет никакой информации о пользователе (еда приходит в контроллер БЕЗ user/userId). Она может быть только авторизованного пользователя, поэтому что-то сравнивать с ним никакого смысла нет. По id еды и авторизованному пользователю нужно проверить ее принадлежность.
        • -
        • 2.2: get\update\delete - следите, чтобы не было NPE (NullPointException может быть, если в хранилище отсутствует узер или еда).
        • +
        • 2.2: get\update\delete - следите, чтобы не было NPE (NullPointException может быть, если в хранилище отсутствует юзер или еда).
        • 2.3: Проверьте сценарий: авторизованный пользователь пробует изменить чужую еду (id еды ему не принадлежит).
        • 2.4: Фильтрацию по датам сделать в репозитории т.к. из базы будем брать сразу отфильтрованные по дням записи. Следите чтобы первый и последний день не были обрезаны, иначе сумма калорий будет неверная.
        • 2.5: Если запрашивается список и он пустой, не возвращайте NULL! По пустому списку можно легко итерироваться без риска NullPoinException.
        • 2.6: Не дублируйте код в getAll и метод с фильтрацией
        • -
        • 2.7: попробуйте учесть, что следующая реализация (сотрировка, фильтрация) будет делаться прямо в базе данных
        • +
        • 2.7: попробуйте учесть, что следующая реализация (сортировка, фильтрация) будет делаться прямо в базе данных
      • 3: Проверьте, что удалили Meal.id, он уже есть в базовом BaseEntity
      • -
      • 4: Проверку isBetweenDate сделать в DateTimeUtil. Попробуйте использовать дженерики и объеденить ее с isBetweenTime
      • +
      • 4: Проверку isBetweenDate сделать в DateTimeUtil. Попробуйте использовать дженерики и объединить ее с isBetweenTime
      • 5: Реализация 'MealRestController' должен уметь обрабатывать запросы:
        • 5.1: Отдать свою еду (для отображения в таблице, формат List<MealWithExceed>), запрос БЕЗ параметров
        • @@ -937,7 +942,7 @@ and beans
        • 7: Авторизированного пользователя берем из AuthorizedUser.id(), cм. ProfileRestController
        • 8: В MealServlet
            -
          • 8.1: Закрывать springContext в сервлете грамотнее всего в HttpServlet.destroy(): если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглетон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно.
          • +
          • 8.1: Закрывать springContext в сервлете грамотнее всего в HttpServlet.destroy(): если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглтон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно.
          • 8.2: Не храните параметры фильтра как члены класса сервлета, это не многопоточно! Один экземпляр сервлета используется всеми запросами на сервер, попробуйте дернуть его из 2х браузеров.
        • @@ -967,7 +972,7 @@ and beans
        -
        +
        @@ -301,7 +301,7 @@
        @@ -468,7 +468,7 @@ @@ -496,9 +496,9 @@
        - Permalink + Permalink - +
        @@ -595,15 +595,15 @@
        - - 9155cc0 + + 9855578 - Dec 21, 2017 + Dec 26, 2017
        @@ -643,12 +643,12 @@ -
        +
        -
        +
        - 153 lines (114 sloc) + 155 lines (116 sloc) - 16.3 KB + 16.8 KB
        @@ -831,7 +831,7 @@ GROUP BY u.id
      • Difference between Spring MVC formatters and converters
      -
    • 4: Протестировать MealRestController через любой инструмент (SoapUi, curl, IDEA Test RESTful Web Service, Postman)
    • +
    • 4: Протестировать MealRestController (SoapUi, curl, IDEA Test RESTful Web Service, Postman). Запросы curl занести в отдельный md файл (либо README.md)

    На следующем занятии используется JavaScript/jQuery. Если у вас там пробелы, пройдите его основы


    @@ -844,6 +844,8 @@ GROUP BY u.id
  • 5: При проблемах с собственным форматтером убедитесь, что в конфигурации <mvc:annotation-driven... не дублируется
  • 6: Проверьте выполение ВСЕХ тестов через maven. В случае проблем проверьте, что не портите константу из MealTestData
  • 7: @Autowired в тестах нужно делать в том месте, где класс будет использоваться. Общий принцип: не размазывать код по классам, объявление переменных держать как можно ближе к ее использованию, группировать (не смешивать) код с разной функциональностью.
  • +
  • 8: Попробуйте в RootControllerTest.testMeals сделать сравнение через model().attribute("meals", expectedValue). Учтите, что вывод результатов через toString к сравнению отношения не имеет
  • +
  • 9: Посмотрите, нет ли в MealTestData методов, которые можно сделать общими (через generic и TestUtil)
  • @@ -871,7 +873,7 @@ GROUP BY u.id \ No newline at end of file diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css index 36305de387ee..279954086eea 100644 --- a/src/main/webapp/resources/css/style.css +++ b/src/main/webapp/resources/css/style.css @@ -29,4 +29,8 @@ header, footer { font-size: 20px; padding: 5px 20px; margin: 6px 0; +} + +.glyphicon { + cursor: pointer; } \ No newline at end of file diff --git a/src/main/webapp/resources/images/icon-meal.png b/src/main/webapp/resources/images/icon-meal.png new file mode 100644 index 0000000000000000000000000000000000000000..b4fc54ad01294abb103d9238c12b1826a9240dfd GIT binary patch literal 1898 zcmV-w2bK7VP)PS&+k>eS5@~O7WLfN z#z>r1)zQ7@JLlXpd}L<$FKLxT1mO7i`0((sD2l48LI|y1MB+H^bUOWhKS`1&t~@IA z1b~^D88e&NXf)c{*(uAi)iCqP{SZP3!OTRY*Xx}A^;#TU>54SHnYXW#g&y6RSh99^HTs$tZ#2`udlC*h;z;d0}zWj3TB}R zFn|flWT07)I)DiT%x1GFidI)w^E?kB5K$Y-9N@(I#>U3x=4P6v0K6d;BWwP3q;Kx$ z_a^B<71hQG`=w-hwO4#}p&VpZ25mf4sA(GS{pHJ-yWQ>y2H8Vl=B=%*jg5^g%R(>^ zpsL+&Chb!3l!F8yFZyq(Lj5wj%J{QS=1FV`-RXv|gz1JiLuH$O`+tkPjc%tTC8 zFh=;G>b&~)rPnv3Ze$)5$8pm%o12^M8_WSlqmlRCIcLNj^|#;a|88fwA6w$=xgmY{ z3@O6{xx>xRAXX4z=JADR>E-A6R2)3HpZxQ}ojuAO`G7P{_xJaw(g~evn6dlC zz2#Zsoia0v$b$zDh=_^k=;$bf;GAP-3;wnBGbR$umb>)D6^v`t0WV%eKS2{4Wl=yZli zQ3XsrIx+ew0zib-1)S^QbOHd(2!?BCD8SOl{xn)BYg1tn0Wg_N*vzV`a?TO4WBc1d zR;d)!V2&CN+Nj4+MfR07QZ`!jIp;_5>jO?C-d9fIKE* zu-I^;zq+&~{XOY%# z5#czF=Rpv~PA_$85Q)&-ghCj?bBuPEs6ZGZPzDp6%=1G+xi%NO7D!e2q_RG|zaIE4tDSi`uGlo$I_gH^9nL%#7Qm zwy>};o6WrUPPl}7=~DWQ)4QejGoqQdqOrEijv55RG{6antne1h4gzPPym{gH%CI-} zh+{V%kK;IQ>CAj(647`(-rL(-UVf&o8)A;F{=@y^)=no$GbJ3?U|1qpi10xPAv8v@ zz){Gm34shDAOyfb1Qrm7fW`-5YB|H<;o)E~NYk{<`O}$Qi%}HC?(S}yru}~3 zIcGo!W-x#-pHg5VCNi_KEDsJ2h-fevsOqEEp9DC8X(Ky0I4FujRns(`7d6kT&U;_i z^>jKd%aVwCy Date: Sun, 31 Dec 2017 21:20:16 +0300 Subject: [PATCH 092/139] 8_07 ajax datatables --- config/messages/app.properties | 1 + config/messages/app_ru.properties | 1 + .../topjava/web/user/AdminAjaxController.java | 37 ++++++++++++ .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 7 +++ src/main/webapp/WEB-INF/jsp/users.jsp | 59 +++++++++++++++++-- .../webapp/resources/js/datatablesUtil.js | 44 ++++++++++++++ .../webapp/resources/js/userDatatables.js | 42 +++++++++++++ 7 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java create mode 100644 src/main/webapp/resources/js/datatablesUtil.js create mode 100644 src/main/webapp/resources/js/userDatatables.js diff --git a/config/messages/app.properties b/config/messages/app.properties index dcd98d4a7bfd..49139dd6f2d0 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -11,6 +11,7 @@ user.email=Email user.roles=Roles user.active=Active user.registered=Registered +user.password=Password meal.title=Meals meal.edit=Edit meal diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 29b654e5bcdf..56673408aac4 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -11,6 +11,7 @@ user.email=\u041F\u043E\u0447\u0442\u0430 user.roles=\u0420\u043E\u043B\u0438 user.active=\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0439 user.registered=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D +user.password=\u041F\u0430\u0440\u043E\u043B\u044C meal.title=\u041C\u043E\u044F \u0435\u0434\u0430 meal.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0435\u0434\u044B diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java new file mode 100644 index 000000000000..db140a52abcb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +@RestController +@RequestMapping("/ajax/admin/users") +public class AdminAjaxController extends AbstractUserController { + + @Override + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + return super.getAll(); + } + + @Override + @DeleteMapping("/{id}") + public void delete(@PathVariable("id") int id) { + super.delete(id); + } + + @PostMapping + public void createOrUpdate(@RequestParam("id") Integer id, + @RequestParam("name") String name, + @RequestParam("email") String email, + @RequestParam("password") String password) { + + User user = new User(id, name, email, password, Role.ROLE_USER); + if (user.isNew()) { + super.create(user); + } + } +} diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index ddd4c21ed5d4..42cf68b4aea4 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -9,5 +9,12 @@ + + + + + + + \ 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 index b73f8f1ffa38..f88a5ca8a97d 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -6,18 +6,19 @@ + +


    - + -

    - +
    @@ -35,15 +36,63 @@ - + - +
    ${user.email} ${user.roles}checked/>checked id="${user.id}"/>
    + + \ No newline at end of file diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js new file mode 100644 index 000000000000..9b1c9307a5f9 --- /dev/null +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -0,0 +1,44 @@ +function makeEditable() { + $(".delete").click(function () { + deleteRow($(this).attr("id")); + }); + + $("#detailsForm").submit(function () { + save(); + return false; + }); +} + +function add() { + $("#detailsForm").find(":input").val(""); + $("#editRow").modal(); +} + +function deleteRow(id) { + $.ajax({ + url: ajaxUrl + id, + type: "DELETE", + success: function () { + updateTable(); + } + }); +} + +function updateTable() { + $.get(ajaxUrl, function (data) { + datatableApi.clear().rows.add(data).draw(); + }); +} + +function save() { + var form = $("#detailsForm"); + $.ajax({ + type: "POST", + url: ajaxUrl, + data: form.serialize(), + success: function () { + $("#editRow").modal("hide"); + updateTable(); + } + }); +} \ No newline at end of file diff --git a/src/main/webapp/resources/js/userDatatables.js b/src/main/webapp/resources/js/userDatatables.js new file mode 100644 index 000000000000..1f9c6e5a190a --- /dev/null +++ b/src/main/webapp/resources/js/userDatatables.js @@ -0,0 +1,42 @@ +var ajaxUrl = "ajax/admin/users/"; +var datatableApi; + +// $(document).ready(function () { +$(function () { + datatableApi = $("#datatable").DataTable({ + "paging": false, + "info": true, + "columns": [ + { + "data": "name" + }, + { + "data": "email" + }, + { + "data": "roles" + }, + { + "data": "enabled" + }, + { + "data": "registered" + }, + { + "defaultContent": "Edit", + "orderable": false + }, + { + "defaultContent": "Delete", + "orderable": false + } + ], + "order": [ + [ + 0, + "asc" + ] + ] + }); + makeEditable(); +}); \ No newline at end of file From ddb44dd06ba63c3152621e71d4cbde3a2e983145 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 31 Dec 2017 21:21:27 +0300 Subject: [PATCH 093/139] 8_08 notification --- .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 2 + .../webapp/resources/js/datatablesUtil.js | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 42cf68b4aea4..2d03b845e5e7 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -10,6 +10,7 @@ + @@ -17,4 +18,5 @@ + \ No newline at end of file diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js index 9b1c9307a5f9..002c3235f346 100644 --- a/src/main/webapp/resources/js/datatablesUtil.js +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -7,6 +7,13 @@ function makeEditable() { save(); return false; }); + + $(document).ajaxError(function (event, jqXHR, options, jsExc) { + failNoty(jqXHR); + }); + + // solve problem with cache in IE: https://stackoverflow.com/a/4303862/548473 + $.ajaxSetup({cache: false}); } function add() { @@ -20,6 +27,7 @@ function deleteRow(id) { type: "DELETE", success: function () { updateTable(); + successNoty("Deleted"); } }); } @@ -39,6 +47,35 @@ function save() { success: function () { $("#editRow").modal("hide"); updateTable(); + successNoty("Saved"); } }); +} + +var failedNote; + +function closeNoty() { + if (failedNote) { + failedNote.close(); + failedNote = undefined; + } +} + +function successNoty(text) { + closeNoty(); + new Noty({ + text: "  " + text, + type: 'success', + layout: "bottomRight", + timeout: 1000 + }).show(); +} + +function failNoty(jqXHR) { + closeNoty(); + failedNote = new Noty({ + text: "  Error status: " + jqXHR.status, + type: "error", + layout: "bottomRight" + }).show(); } \ No newline at end of file From 5ac12d1f24e7fa4b9d70fe7c62aa0f17b1031ac2 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 31 Dec 2017 21:21:48 +0300 Subject: [PATCH 094/139] 8_09 add security --- config/curl.md | 18 ++++++------ pom.xml | 13 +++++++++ src/main/resources/logback.xml | 1 + src/main/resources/spring/spring-app.xml | 8 +----- src/main/resources/spring/spring-security.xml | 28 +++++++++++++++++++ src/main/webapp/WEB-INF/web.xml | 10 +++++++ 6 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 src/main/resources/spring/spring-security.xml diff --git a/config/curl.md b/config/curl.md index c6bb0b8ebcc7..8e197ec254bb 100644 --- a/config/curl.md +++ b/config/curl.md @@ -2,28 +2,28 @@ > For windows use `Git Bash` #### get All Users -`curl -s http://localhost:8080/topjava/rest/admin/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` +`curl -s http://localhost:8080/topjava/rest/admin/users/100001 --user admin@gmail.com:admin` #### get All Meals -`curl -s http://localhost:8080/topjava/rest/profile/meals` +`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` +`curl -s http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` #### filter Meals -`curl -s "http://localhost:8080/topjava/rest/profile/meals/filter?startDate=2015-05-30&startTime=07:00:00&endDate=2015-05-31&endTime=11:00:00"` +`curl -s "http://localhost:8080/topjava/rest/profile/meals/filter?startDate=2015-05-30&startTime=07:00:00&endDate=2015-05-31&endTime=11:00:00" --user user@yandex.ru:password` #### get Meals not found -`curl -s -v http://localhost:8080/topjava/rest/profile/meals/100008` +`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` +`curl -s -X DELETE http://localhost:8080/topjava/rest/profile/meals/100002 --user user@yandex.ru:password` #### create Meals -`curl -s -X POST -d '{"dateTime":"2015-06-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals` +`curl -s -X POST -d '{"dateTime":"2015-06-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` #### update Meals -`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003` \ No newline at end of file +`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` \ No newline at end of file diff --git a/pom.xml b/pom.xml index ca06b1a562c8..c1fc015306c4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ 8.5.24 5.0.1.RELEASE 2.0.2.RELEASE + 5.0.0.RELEASE 2.9.3 @@ -177,6 +178,18 @@ ${spring-data-jpa.version} + + + org.springframework.security + spring-security-web + ${spring-security.version} + + + org.springframework.security + spring-security-config + ${spring-security.version} + + org.hibernate diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 7f5b7b049490..dc5b67763c59 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -23,6 +23,7 @@ + diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index 689a9e68cd0b..b669b4c80685 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -4,14 +4,8 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> - + diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml new file mode 100644 index 000000000000..dc2f5114db94 --- /dev/null +++ b/src/main/resources/spring/spring-security.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 91d304f3c7d1..c471a447ca14 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -37,6 +37,16 @@ / + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + springSecurityFilterChain + /* + + encodingFilter org.springframework.web.filter.CharacterEncodingFilter From c3550765d24cf9a95b3a51301bf7b9e6060bf25e Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 10 Jan 2018 22:14:30 +0300 Subject: [PATCH 095/139] 9_0 fix --- pom.xml | 4 ++-- .../javawebinar/topjava/web/converter/DateTimeFormatters.java | 4 ---- .../ru/javawebinar/topjava/web/meal/MealRestController.java | 2 ++ .../ru/javawebinar/topjava/web/user/AdminRestController.java | 2 ++ .../javawebinar/topjava/web/user/ProfileRestController.java | 2 ++ .../javawebinar/topjava/web/meal/MealRestControllerTest.java | 2 +- .../javawebinar/topjava/web/user/AdminRestControllerTest.java | 2 +- .../topjava/web/user/ProfileRestControllerTest.java | 2 +- 8 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index c1fc015306c4..2a404c641179 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ UTF-8 8.5.24 - 5.0.1.RELEASE + 5.0.2.RELEASE 2.0.2.RELEASE 5.0.0.RELEASE 2.9.3 @@ -35,7 +35,7 @@ 5.2.12.Final - 6.0.5.Final + 6.0.7.Final 3.0.1-b08 diff --git a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java index c308ec9df57f..6e4492724aff 100644 --- a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java +++ b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java @@ -11,10 +11,6 @@ import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; -/** - * gkislin - * 25.10.2016 - */ public class DateTimeFormatters { public static class LocalDateFormatter implements Formatter { diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index 7362ecaed39d..c395cbb5787d 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.web.meal; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -25,6 +26,7 @@ public Meal get(@PathVariable("id") int id) { @Override @DeleteMapping("/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) public void delete(@PathVariable("id") int id) { super.delete(id); } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index 1ad69faff6c3..fe869a482bf5 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -1,5 +1,6 @@ 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.*; @@ -42,6 +43,7 @@ public ResponseEntity createWithLocation(@RequestBody User user) { @Override @DeleteMapping(value = "/{id}") + @ResponseStatus(value = HttpStatus.NO_CONTENT) public void delete(@PathVariable("id") int id) { super.delete(id); } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index 3b378d91803a..1def55442c56 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.AuthorizedUser; @@ -16,6 +17,7 @@ public User get() { } @DeleteMapping + @ResponseStatus(value = HttpStatus.NO_CONTENT) public void delete() { super.delete(AuthorizedUser.id()); } diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java index f44606029637..89213d0bfb84 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -42,7 +42,7 @@ public void testGet() throws Exception { @Test public void testDelete() throws Exception { mockMvc.perform(delete(REST_URL + MEAL1_ID)) - .andExpect(status().isOk()); + .andExpect(status().isNoContent()); assertMatch(service.getAll(START_SEQ), MEAL6, MEAL5, MEAL4, MEAL3, MEAL2); } diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index daa29d07cbf0..3543a41c88a7 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -43,7 +43,7 @@ public void testGetByEmail() throws Exception { public void testDelete() throws Exception { mockMvc.perform(delete(REST_URL + USER_ID)) .andDo(print()) - .andExpect(status().isOk()); + .andExpect(status().isNoContent()); assertMatch(userService.getAll(), ADMIN); } diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java index c97b309ffa2f..59ae122f99f6 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -30,7 +30,7 @@ public void testGet() throws Exception { @Test public void testDelete() throws Exception { mockMvc.perform(delete(REST_URL)) - .andExpect(status().isOk()); + .andExpect(status().isNoContent()); assertMatch(userService.getAll(), ADMIN); } From 177ef5ce2b714b1e0e81484c5d37c8e35bde3a64 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 10 Jan 2018 22:22:53 +0300 Subject: [PATCH 096/139] 9_1 HW08 --- config/messages/app.properties | 8 +- config/messages/app_ru.properties | 9 +- .../topjava/web/meal/JspMealController.java | 70 ------- .../topjava/web/meal/MealAjaxController.java | 50 +++++ src/main/webapp/WEB-INF/jsp/mealForm.jsp | 34 ---- src/main/webapp/WEB-INF/jsp/meals.jsp | 183 +++++++++++++----- src/main/webapp/WEB-INF/jsp/users.jsp | 6 +- src/main/webapp/resources/css/style.css | 25 +-- .../webapp/resources/js/datatablesUtil.js | 21 +- .../webapp/resources/js/mealDatatables.js | 44 +++++ .../webapp/resources/js/userDatatables.js | 4 + 11 files changed, 245 insertions(+), 209 deletions(-) delete mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java create mode 100644 src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java delete mode 100644 src/main/webapp/WEB-INF/jsp/mealForm.jsp create mode 100644 src/main/webapp/resources/js/mealDatatables.js diff --git a/config/messages/app.properties b/config/messages/app.properties index 49139dd6f2d0..cb364673d06e 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -1,5 +1,4 @@ app.title=Calories management -app.home=Home app.footer=Project Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery application app.login=Login as @@ -16,7 +15,6 @@ user.password=Password meal.title=Meals meal.edit=Edit meal meal.add=Add meal -meal.filter=Filter meal.startDate=From date meal.endDate=To date meal.startTime=From time @@ -26,8 +24,4 @@ meal.dateTime=Date/Time meal.calories=Calories common.add=Add -common.select=Select -common.delete=Delete -common.update=Update -common.save=Save -common.cancel=Cancel \ No newline at end of file +common.select=Select \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 56673408aac4..8bdcfdc8861b 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -1,5 +1,4 @@ app.title=\u041F\u043E\u0434\u0441\u0447\u0435\u0442 \u043A\u0430\u043B\u043E\u0440\u0438\u0439 -app.home=\u0413\u043B\u0430\u0432\u043D\u0430\u044F app.footer=\u041F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0443 Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery app.login=\u0417\u0430\u0439\u0442\u0438 \u043A\u0430\u043A @@ -16,7 +15,6 @@ user.password=\u041F\u0430\u0440\u043E\u043B\u044C meal.title=\u041C\u043E\u044F \u0435\u0434\u0430 meal.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0435\u0434\u044B meal.add=\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0435\u0434\u044B -meal.filter=\u041E\u0442\u0444\u0438\u043B\u044C\u0442\u0440\u043E\u0432\u0430\u0442\u044C meal.startDate=\u041E\u0442 \u0434\u0430\u0442\u044B meal.endDate=\u0414\u043E \u0434\u0430\u0442\u044B meal.startTime=\u041E\u0442 \u0432\u0440\u0435\u043C\u0435\u043D\u0438 @@ -26,9 +24,4 @@ meal.dateTime=\u0414\u0430\u0442\u0430/\u0412\u0440\u0435\u043C\u044F meal.calories=\u041A\u0430\u043B\u043E\u0440\u0438\u0438 common.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C -common.select=\u0412\u044B\u0431\u0440\u0430\u0442\u044C -common.delete=\u0423\u0434\u0430\u043B\u0438\u0442\u044C -common.update=\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C -common.save=\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C -common.cancel=\u041E\u0442\u043C\u0435\u043D\u0430 -common.actions=\u0412\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \ No newline at end of file +common.select=\u0412\u044B\u0431\u0440\u0430\u0442\u044C \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java deleted file mode 100644 index 527f936e60b0..000000000000 --- a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java +++ /dev/null @@ -1,70 +0,0 @@ -package ru.javawebinar.topjava.web.meal; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import ru.javawebinar.topjava.model.Meal; - -import javax.servlet.http.HttpServletRequest; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.temporal.ChronoUnit; -import java.util.Objects; - -import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; -import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; - -@Controller -@RequestMapping(value = "/meals") -public class JspMealController extends AbstractMealController { - - @GetMapping("/delete") - public String delete(HttpServletRequest request) { - super.delete(getId(request)); - return "redirect:/meals"; - } - - @GetMapping("/update") - public String update(HttpServletRequest request, Model model) { - model.addAttribute("meal", super.get(getId(request))); - return "mealForm"; - } - - @GetMapping("/create") - public String create(Model model) { - model.addAttribute("meal", new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS), "", 1000)); - return "mealForm"; - } - - @PostMapping - public String updateOrCreate(HttpServletRequest request) { - Meal meal = new Meal(LocalDateTime.parse(request.getParameter("dateTime")), - request.getParameter("description"), - Integer.valueOf(request.getParameter("calories"))); - - if (request.getParameter("id").isEmpty()) { - super.create(meal); - } else { - super.update(meal, getId(request)); - } - return "redirect:/meals"; - } - - @PostMapping("/filter") - public String getBetween(HttpServletRequest request, Model model) { - LocalDate startDate = parseLocalDate(request.getParameter("startDate")); - LocalDate endDate = parseLocalDate(request.getParameter("endDate")); - LocalTime startTime = parseLocalTime(request.getParameter("startTime")); - LocalTime endTime = parseLocalTime(request.getParameter("endTime")); - model.addAttribute("meals", super.getBetween(startDate, startTime, endDate, endTime)); - return "meals"; - } - - private int getId(HttpServletRequest request) { - String paramId = Objects.requireNonNull(request.getParameter("id")); - return Integer.valueOf(paramId); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java new file mode 100644 index 000000000000..898ca3864cc5 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java @@ -0,0 +1,50 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealWithExceed; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@RestController +@RequestMapping(value = "/ajax/profile/meals") +public class MealAjaxController extends AbstractMealController { + + @Override + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + return super.getAll(); + } + + @Override + @DeleteMapping(value = "/{id}") + public void delete(@PathVariable("id") int id) { + super.delete(id); + } + + @PostMapping + public void createOrUpdate(@RequestParam("id") Integer id, + @RequestParam("dateTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime dateTime, + @RequestParam("description") String description, + @RequestParam("calories") int calories) { + Meal meal = new Meal(id, dateTime, description, calories); + if (meal.isNew()) { + super.create(meal); + } + } + + @Override + @PostMapping(value = "/filter", produces = MediaType.APPLICATION_JSON_VALUE) + public List getBetween( + @RequestParam(value = "startDate", required = false) LocalDate startDate, + @RequestParam(value = "startTime", required = false) LocalTime startTime, + @RequestParam(value = "endDate", required = false) LocalDate endDate, + @RequestParam(value = "endTime", required = false) LocalTime endTime) { + return super.getBetween(startDate, startTime, endDate, endTime); + } +} \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/mealForm.jsp b/src/main/webapp/WEB-INF/jsp/mealForm.jsp deleted file mode 100644 index 999c1e216ddb..000000000000 --- a/src/main/webapp/WEB-INF/jsp/mealForm.jsp +++ /dev/null @@ -1,34 +0,0 @@ -<%@ 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" %> - - - - - - -
    - -

    -
    -
    - -
    -
    :
    -
    -
    -
    -
    :
    -
    -
    -
    -
    :
    -
    -
    - - -
    -
    - - - diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp index 65ceea40ad7c..a267bdcabe3b 100644 --- a/src/main/webapp/WEB-INF/jsp/meals.jsp +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -5,60 +5,143 @@ + + -
    -

    +
    +
    +

    -
    -
    -
    :
    -
    -
    -
    -
    :
    -
    -
    -
    -
    :
    -
    -
    -
    -
    :
    -
    -
    - -
    -
    - -
    - - - - - - - - - - - - - - - - - - +
    +
    +
    +
    +
    +
    + + +
    + +
    + + + +
    + +
    +
    +
    + + +
    + +
    + + + +
    + +
    +
    + +
    + +
    +
    +
    + + + + +
    - <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> - <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> - <%--${fn:replace(meal.dateTime, 'T', ' ')}--%> - ${fn:formatDateTime(meal.dateTime)} - ${meal.description}${meal.calories}
    + + + + + + + - -
    -
    + + + + + + <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> + <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> + <%--${fn:replace(meal.dateTime, 'T', ' ')}--%> + ${fn:formatDateTime(meal.dateTime)} + + ${meal.description} + ${meal.calories} + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index f88a5ca8a97d..c17165745ebc 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -39,7 +39,9 @@ checked id="${user.id}"/> - + + + @@ -83,7 +85,7 @@
    -
    diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css index 279954086eea..3dc625f258ee 100644 --- a/src/main/webapp/resources/css/style.css +++ b/src/main/webapp/resources/css/style.css @@ -1,20 +1,3 @@ -dl { - background: none repeat scroll 0 0 #FAFAFA; - margin: 8px 0; - padding: 0; -} - -dt { - display: inline-block; - width: 170px; -} - -dd { - display: inline-block; - margin-left: 8px; - vertical-align: top; -} - .normal { color: green; } @@ -23,12 +6,8 @@ dd { color: red; } -header, footer { - background: none repeat scroll 0 0 #A6C9E2; - color: #2E6E9E; - font-size: 20px; - padding: 5px 20px; - margin: 6px 0; +.footer { + padding-bottom: 15px; } .glyphicon { diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js index 002c3235f346..4d26337dcb3e 100644 --- a/src/main/webapp/resources/js/datatablesUtil.js +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -1,13 +1,7 @@ -function makeEditable() { - $(".delete").click(function () { - deleteRow($(this).attr("id")); - }); - - $("#detailsForm").submit(function () { - save(); - return false; - }); +var form; +function makeEditable() { + form = $('#detailsForm'); $(document).ajaxError(function (event, jqXHR, options, jsExc) { failNoty(jqXHR); }); @@ -17,7 +11,7 @@ function makeEditable() { } function add() { - $("#detailsForm").find(":input").val(""); + form.find(":input").val(""); $("#editRow").modal(); } @@ -32,14 +26,11 @@ function deleteRow(id) { }); } -function updateTable() { - $.get(ajaxUrl, function (data) { - datatableApi.clear().rows.add(data).draw(); - }); +function updateTableByData(data) { + datatableApi.clear().rows.add(data).draw(); } function save() { - var form = $("#detailsForm"); $.ajax({ type: "POST", url: ajaxUrl, diff --git a/src/main/webapp/resources/js/mealDatatables.js b/src/main/webapp/resources/js/mealDatatables.js new file mode 100644 index 000000000000..165ea9b67e03 --- /dev/null +++ b/src/main/webapp/resources/js/mealDatatables.js @@ -0,0 +1,44 @@ +var ajaxUrl = "ajax/profile/meals/"; +var datatableApi; + +function updateTable() { + $.ajax({ + type: "POST", + url: ajaxUrl + "filter", + data: $("#filter").serialize(), + success: updateTableByData + }); +} + +$(function () { + datatableApi = $("#datatable").DataTable({ + "paging": false, + "info": true, + "columns": [ + { + "data": "dateTime" + }, + { + "data": "description" + }, + { + "data": "calories" + }, + { + "defaultContent": "Edit", + "orderable": false + }, + { + "defaultContent": "Delete", + "orderable": false + } + ], + "order": [ + [ + 0, + "desc" + ] + ] + }); + makeEditable(); +}); \ No newline at end of file diff --git a/src/main/webapp/resources/js/userDatatables.js b/src/main/webapp/resources/js/userDatatables.js index 1f9c6e5a190a..24de4a187e43 100644 --- a/src/main/webapp/resources/js/userDatatables.js +++ b/src/main/webapp/resources/js/userDatatables.js @@ -1,6 +1,10 @@ var ajaxUrl = "ajax/admin/users/"; var datatableApi; +function updateTable() { + $.get(ajaxUrl, updateTableByData); +} + // $(document).ready(function () { $(function () { datatableApi = $("#datatable").DataTable({ From 948e9634fc58a8e84c3dfbbf82de3c4b38475235 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 10 Jan 2018 22:23:25 +0300 Subject: [PATCH 097/139] 9_02 HW08 clear filter --- src/main/webapp/WEB-INF/jsp/meals.jsp | 3 +++ src/main/webapp/resources/js/mealDatatables.js | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp index a267bdcabe3b..871d546122f8 100644 --- a/src/main/webapp/WEB-INF/jsp/meals.jsp +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -51,6 +51,9 @@
    diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js index d09b349b243e..6b2df620a25a 100644 --- a/src/main/webapp/resources/js/datatablesUtil.js +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -76,4 +76,18 @@ function failNoty(jqXHR) { 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/userDatatables.js b/src/main/webapp/resources/js/userDatatables.js index da051b59f703..5c9dc8a9edc6 100644 --- a/src/main/webapp/resources/js/userDatatables.js +++ b/src/main/webapp/resources/js/userDatatables.js @@ -23,6 +23,10 @@ function enable(chkbox, id) { // $(document).ready(function () { $(function () { datatableApi = $("#datatable").DataTable({ + "ajax": { + "url": ajaxUrl, + "dataSrc": "" + }, "paging": false, "info": true, "columns": [ @@ -30,24 +34,44 @@ $(function () { "data": "name" }, { - "data": "email" + "data": "email", + "render": function (data, type, row) { + if (type === "display") { + return "" + data + ""; + } + return data; + } }, { "data": "roles" }, { - "data": "enabled" + "data": "enabled", + "render": function (data, type, row) { + if (type === "display") { + return ""; + } + return data; + } }, { - "data": "registered" + "data": "registered", + "render": function (date, type, row) { + if (type === "display") { + return date.substring(0, 10); + } + return date; + } }, { - "defaultContent": "Edit", - "orderable": false + "orderable": false, + "defaultContent": "", + "render": renderEditBtn }, { - "defaultContent": "Delete", - "orderable": false + "orderable": false, + "defaultContent": "", + "render": renderDeleteBtn } ], "order": [ @@ -55,7 +79,12 @@ $(function () { 0, "asc" ] - ] + ], + "createdRow": function (row, data, dataIndex) { + if (!data.enabled) { + $(row).addClass("disabled"); + } + }, + "initComplete": makeEditable }); - makeEditable(); }); \ No newline at end of file From cf60461497e49eed16fb328ca1410febeffe0efb Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 14 Jan 2018 13:45:20 +0300 Subject: [PATCH 104/139] 9_08 js i18n --- config/messages/app.properties | 7 ++++++- config/messages/app_ru.properties | 7 ++++++- src/main/webapp/WEB-INF/jsp/users.jsp | 11 ++++++++++- src/main/webapp/resources/js/datatablesUtil.js | 12 +++++++----- src/main/webapp/resources/js/userDatatables.js | 2 +- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/config/messages/app.properties b/config/messages/app.properties index cb364673d06e..8050dfc0fd03 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -24,4 +24,9 @@ meal.dateTime=Date/Time meal.calories=Calories common.add=Add -common.select=Select \ No newline at end of file +common.select=Select +common.deleted=Record deleted +common.saved=Record saved +common.enabled=Record enabled +common.disabled=Record disabled +common.errorStatus=Error status \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 8bdcfdc8861b..c5bbcd6d81c6 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -24,4 +24,9 @@ meal.dateTime=\u0414\u0430\u0442\u0430/\u0412\u0440\u0435\u043C\u044F meal.calories=\u041A\u0430\u043B\u043E\u0440\u0438\u0438 common.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C -common.select=\u0412\u044B\u0431\u0440\u0430\u0442\u044C \ No newline at end of file +common.select=\u0412\u044B\u0431\u0440\u0430\u0442\u044C +common.deleted=\u0417\u0430\u043F\u0438\u0441\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u0430 +common.saved=\u0417\u0430\u043F\u0438\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 +common.enabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430 +common.disabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0434\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430 +common.errorStatus=\u0421\u0442\u0430\u0442\u0443\u0441 \u043E\u0448\u0438\u0431\u043A\u0438 \ 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 index 3ab88e59d7e5..57e3c0de9bdf 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -39,7 +39,7 @@ From 60c9ab3381734828ee1a34ed6e118bf3e42165f0 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 14 Jan 2018 13:47:23 +0300 Subject: [PATCH 106/139] 9_10 jsp form login --- config/messages/app.properties | 2 +- config/messages/app_ru.properties | 2 +- .../topjava/web/RootController.java | 13 +-- src/main/resources/spring/spring-mvc.xml | 9 +- src/main/resources/spring/spring-security.xml | 9 +- src/main/webapp/WEB-INF/jsp/index.jsp | 28 ------ src/main/webapp/WEB-INF/jsp/login.jsp | 92 +++++++++++++++++++ src/main/webapp/resources/css/style.css | 18 ++++ src/main/webapp/test.html | 13 +++ 9 files changed, 142 insertions(+), 44 deletions(-) delete mode 100644 src/main/webapp/WEB-INF/jsp/index.jsp create mode 100644 src/main/webapp/WEB-INF/jsp/login.jsp create mode 100644 src/main/webapp/test.html diff --git a/config/messages/app.properties b/config/messages/app.properties index 8050dfc0fd03..7deaf8f14a4b 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -1,6 +1,6 @@ app.title=Calories management app.footer=Project Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery application -app.login=Login as +app.enter=Enter as user.title=Users user.edit=Edit user diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index c5bbcd6d81c6..bd7d865c6f11 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -1,6 +1,6 @@ app.title=\u041F\u043E\u0434\u0441\u0447\u0435\u0442 \u043A\u0430\u043B\u043E\u0440\u0438\u0439 app.footer=\u041F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0443 Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery -app.login=\u0417\u0430\u0439\u0442\u0438 \u043A\u0430\u043A +app.enter=\u0412\u043E\u0439\u0442\u0438 \u043A\u0430\u043A user.title=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 user.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java index 8914925f952c..f7be0f1c2309 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -4,13 +4,10 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; import ru.javawebinar.topjava.AuthorizedUser; import ru.javawebinar.topjava.service.MealService; import ru.javawebinar.topjava.util.MealsUtil; -import javax.servlet.http.HttpServletRequest; - @Controller public class RootController { @@ -19,7 +16,7 @@ public class RootController { @GetMapping("/") public String root() { - return "index"; + return "redirect:meals"; } @GetMapping("/users") @@ -27,11 +24,9 @@ public String users() { return "users"; } - @PostMapping("/users") - public String setUser(HttpServletRequest request) { - int userId = Integer.valueOf(request.getParameter("userId")); - AuthorizedUser.setId(userId); - return "redirect:meals"; + @GetMapping(value = "/login") + public String login() { + return "login"; } @GetMapping("/meals") diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 671d73571d8e..f5b51c0a82c6 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -1,12 +1,15 @@ - + + + diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml index 844e77310c4c..0b553b8394d5 100644 --- a/src/main/resources/spring/spring-security.xml +++ b/src/main/resources/spring/spring-security.xml @@ -17,10 +17,15 @@ + + - - + + + diff --git a/src/main/webapp/WEB-INF/jsp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp deleted file mode 100644 index 5595a2024e3f..000000000000 --- a/src/main/webapp/WEB-INF/jsp/index.jsp +++ /dev/null @@ -1,28 +0,0 @@ -<%@ 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" %> - - - - - -
    -
    -

    - -

    - : - - -
      -
    • -
    • -
    -
    -
    - - - \ 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..b515221532b9 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/login.jsp @@ -0,0 +1,92 @@ +<%@ 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" %> + + + + + + +
    +
    + +
    + ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message} +
    +
    + +
    + +
    +
    +
    +

    + + +

    +
    +

    Стек технологий: Spring Security, + Spring MVC, + Spring Data JPA, + Spring Security + Test, + Hibernate ORM, + Hibernate Validator, + SLF4J, + Json Jackson, + JSP, + JSTL, + Apache Tomcat, + WebJars, + DataTables plugin, + EHCACHE, + PostgreSQL, + JUnit, + Hamcrest, + jQuery, + jQuery notification, + Bootstrap.

    +
    +
    +
    +
    +    Java Enterprise проект с + регистрацией/авторизацией и интерфейсом на основе ролей (USER, ADMIN). + Администратор может создавать/редактировать/удалять пользователей, а пользователи - управлять своим + профилем и данными (день, еда, калории) через UI (по AJAX) и по REST интерфейсу с базовой авторизацией. + Возможна фильтрация данных по датам и времени, при этом цвет записи таблицы еды зависит от того, превышает ли + сумма + калорий за день норму (редактируемый параметр в профиле пользователя). + Весь REST интерфейс покрывается JUnit тестами, используя Spring MVC Test и Spring Security Test. +
    +
    + + + + \ No newline at end of file diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css index df82f4976793..bb06ce993a36 100644 --- a/src/main/webapp/resources/css/style.css +++ b/src/main/webapp/resources/css/style.css @@ -14,6 +14,24 @@ 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; +} + .glyphicon { cursor: pointer; } \ 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..62e151e5806f --- /dev/null +++ b/src/main/webapp/test.html @@ -0,0 +1,13 @@ + + + + + +
    + +
    + + \ No newline at end of file From be56fa7d3874ad077f71de3b2814ab87f9c61443 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 14 Jan 2018 13:49:14 +0300 Subject: [PATCH 107/139] 9_11 auth via user service --- .../javawebinar/topjava/AuthorizedUser.java | 49 +++++++++++++++---- .../ru/javawebinar/topjava/model/Role.java | 11 ++++- .../topjava/service/UserServiceImpl.java | 16 +++++- .../ru/javawebinar/topjava/to/UserTo.java | 20 +++++++- .../ru/javawebinar/topjava/util/UserUtil.java | 4 ++ src/main/resources/spring/spring-security.xml | 9 +++- .../web/user/ProfileRestControllerTest.java | 2 +- 7 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java index c5ba78487453..d3d468ec21a4 100644 --- a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java +++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java @@ -1,21 +1,52 @@ package ru.javawebinar.topjava; -import ru.javawebinar.topjava.model.AbstractBaseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; -import static ru.javawebinar.topjava.util.UserUtil.DEFAULT_CALORIES_PER_DAY; +import static java.util.Objects.requireNonNull; -public class AuthorizedUser { - private static int id = AbstractBaseEntity.START_SEQ; +public class AuthorizedUser extends org.springframework.security.core.userdetails.User { + private static final long serialVersionUID = 1L; - public static int id() { - return id; + private final UserTo userTo; + + public AuthorizedUser(User user) { + super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles()); + this.userTo = UserUtil.asTo(user); + } + + 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 void setId(int id) { - AuthorizedUser.id = id; + public static AuthorizedUser get() { + AuthorizedUser user = safeGet(); + requireNonNull(user, "No authorized user found"); + return user; + } + + public static int id() { + return get().userTo.getId(); } public static int getCaloriesPerDay() { - return DEFAULT_CALORIES_PER_DAY; + return get().userTo.getCaloriesPerDay(); + } + + 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/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java index 84d62071ad9c..e7c8739712d1 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Role.java +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -1,6 +1,13 @@ package ru.javawebinar.topjava.model; -public enum Role { +import org.springframework.security.core.GrantedAuthority; + +public enum Role implements GrantedAuthority { ROLE_USER, - ROLE_ADMIN + ROLE_ADMIN; + + @Override + public String getAuthority() { + return name(); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java index 11ce90a1c5c5..a28a2cb27957 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java @@ -3,9 +3,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.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; @@ -17,8 +20,8 @@ import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; -@Service -public class UserServiceImpl implements UserService { +@Service("userService") +public class UserServiceImpl implements UserService, UserDetailsService { private final UserRepository repository; @@ -82,6 +85,15 @@ public void enable(int id, boolean enabled) { repository.save(user); } + @Override + public AuthorizedUser loadUserByUsername(String email) throws UsernameNotFoundException { + User user = repository.getByEmail(email.toLowerCase()); + if (user == null) { + throw new UsernameNotFoundException("User " + email + " is not found"); + } + return new AuthorizedUser(user); + } + @Override public User getWithMeals(int id) { return checkNotFoundWithId(repository.getWithMeals(id), id); diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java index 975801eb1fd1..d4833fcffeea 100644 --- a/src/main/java/ru/javawebinar/topjava/to/UserTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -1,10 +1,16 @@ 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 { +public class UserTo extends BaseTo implements Serializable { + private static final long serialVersionUID = 1L; @NotBlank private String name; @@ -16,14 +22,19 @@ public class UserTo extends BaseTo { @Size(min = 5, max = 32, message = "length must 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) { + 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() { @@ -50,12 +61,17 @@ public void setEmail(String email) { this.email = email; } + 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/UserUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java index 9a3c0e9cbf56..e2dfdd5a9a45 100644 --- a/src/main/java/ru/javawebinar/topjava/util/UserUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java @@ -12,6 +12,10 @@ public static User createNewFromTo(UserTo newUser) { return new User(null, newUser.getName(), newUser.getEmail().toLowerCase(), newUser.getPassword(), Role.ROLE_USER); } + public static UserTo asTo(User user) { + return new UserTo(user.getId(), user.getName(), user.getEmail(), user.getPassword(), user.getCaloriesPerDay()); + } + public static User updateFromTo(User user, UserTo userTo) { user.setName(userTo.getName()); user.setEmail(userTo.getEmail().toLowerCase()); diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml index 0b553b8394d5..cece85fda954 100644 --- a/src/main/resources/spring/spring-security.xml +++ b/src/main/resources/spring/spring-security.xml @@ -30,12 +30,19 @@ - + + \ 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 index 7dde6e1e90f0..3a46d6f6e382 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -37,7 +37,7 @@ public void testDelete() throws Exception { @Test public void testUpdate() throws Exception { - UserTo updatedTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword"); + UserTo updatedTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500); mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON) .content(JsonUtil.writeValue(updatedTo))) From 8bb414d1f83c0833c95b49b5bc00de5b57ab4655 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 14 Jan 2018 13:51:49 +0300 Subject: [PATCH 108/139] 9_12 spring security test --- pom.xml | 5 +++ .../ru/javawebinar/topjava/SpringMain.java | 5 +++ .../java/ru/javawebinar/topjava/TestUtil.java | 18 +++++++++++ .../topjava/web/AbstractControllerTest.java | 3 ++ .../topjava/web/RootControllerTest.java | 31 ++++++++++--------- .../web/user/AdminRestControllerTest.java | 30 +++++++++++++++--- .../web/user/ProfileRestControllerTest.java | 14 +++++++-- 7 files changed, 84 insertions(+), 22 deletions(-) diff --git a/pom.xml b/pom.xml index 2a404c641179..548d7e83bc99 100644 --- a/pom.xml +++ b/pom.xml @@ -345,6 +345,11 @@ 3.8.0 test
    + + org.springframework.security + spring-security-test + ${spring-security.version} + diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java index 39331e507792..b3123f44550a 100644 --- a/src/test/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -13,6 +13,9 @@ 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 @@ -21,6 +24,8 @@ public static void main(String[] args) { appCtx.load("spring/spring-app.xml", "spring/mock.xml"); appCtx.refresh(); + mockAuthorize(USER); + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ROLE_ADMIN)); diff --git a/src/test/java/ru/javawebinar/topjava/TestUtil.java b/src/test/java/ru/javawebinar/topjava/TestUtil.java index 15361c1a28b0..5b681ca142a3 100644 --- a/src/test/java/ru/javawebinar/topjava/TestUtil.java +++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java @@ -1,7 +1,12 @@ 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.ResultActions; import org.springframework.test.web.servlet.ResultMatcher; +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; @@ -31,4 +36,17 @@ public static ResultMatcher contentJson(T expected) { public static ResultMatcher contentJsonArray(T... expected) { return contentJson(expected); } + + 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/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java index f2e7bb0e3db4..d881384f4b80 100644 --- a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -19,6 +19,8 @@ import javax.annotation.PostConstruct; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + @ContextConfiguration({ "classpath:spring/spring-app.xml", "classpath:spring/spring-mvc.xml", @@ -56,6 +58,7 @@ private void postConstruct() { mockMvc = MockMvcBuilders .webAppContextSetup(webApplicationContext) .addFilter(CHARACTER_ENCODING_FILTER) + .apply(springSecurity()) .build(); } diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java index 597f893197a5..9fed82a03c04 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -1,43 +1,44 @@ package ru.javawebinar.topjava.web; import org.junit.Test; -import ru.javawebinar.topjava.AuthorizedUser; import ru.javawebinar.topjava.MealTestData; import ru.javawebinar.topjava.util.MealsUtil; -import static org.hamcrest.Matchers.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static ru.javawebinar.topjava.TestUtil.userAuth; +import static ru.javawebinar.topjava.UserTestData.ADMIN; import static ru.javawebinar.topjava.UserTestData.USER; -import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class RootControllerTest extends AbstractControllerTest { @Test public void testUsers() throws Exception { - mockMvc.perform(get("/users")) + mockMvc.perform(get("/users") + .with(userAuth(ADMIN))) .andDo(print()) .andExpect(status().isOk()) .andExpect(view().name("users")) - .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) - .andExpect(model().attribute("users", hasSize(2))) - .andExpect(model().attribute("users", hasItem( - allOf( - hasProperty("id", is(START_SEQ)), - hasProperty("name", is(USER.getName())) - ) - ))); + .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")); + } + + @Test + public void testUnAuth() throws Exception { + mockMvc.perform(get("/users")) + .andDo(print()) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); } @Test public void testMeals() throws Exception { - mockMvc.perform(get("/meals")) + mockMvc.perform(get("/meals") + .with(userAuth(USER))) .andDo(print()) - .andExpect(status().isOk()) .andExpect(view().name("meals")) .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp")) - .andExpect(model().attribute("meals", MealsUtil.getWithExceeded(MealTestData.MEALS, AuthorizedUser.getCaloriesPerDay()))); + .andExpect(model().attribute("meals", MealsUtil.getWithExceeded(MealTestData.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 index 3543a41c88a7..ba4709e502a8 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -15,6 +15,7 @@ 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.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; public class AdminRestControllerTest extends AbstractControllerTest { @@ -23,7 +24,8 @@ public class AdminRestControllerTest extends AbstractControllerTest { @Test public void testGet() throws Exception { - mockMvc.perform(get(REST_URL + ADMIN_ID)) + mockMvc.perform(get(REST_URL + ADMIN_ID) + .with(userHttpBasic(ADMIN))) .andExpect(status().isOk()) .andDo(print()) // https://jira.spring.io/browse/SPR-14472 @@ -33,20 +35,35 @@ public void testGet() throws Exception { @Test public void testGetByEmail() throws Exception { - mockMvc.perform(get(REST_URL + "by?email=" + USER.getEmail())) + mockMvc.perform(get(REST_URL + "by?email=" + ADMIN.getEmail()) + .with(userHttpBasic(ADMIN))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(contentJson(USER)); + .andExpect(contentJson(ADMIN)); } @Test public void testDelete() throws Exception { - mockMvc.perform(delete(REST_URL + USER_ID)) + mockMvc.perform(delete(REST_URL + USER_ID) + .with(userHttpBasic(ADMIN))) .andDo(print()) .andExpect(status().isNoContent()); assertMatch(userService.getAll(), ADMIN); } + @Test + public void testGetUnauth() throws Exception { + mockMvc.perform(get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void testGetForbidden() throws Exception { + mockMvc.perform(get(REST_URL) + .with(userHttpBasic(USER))) + .andExpect(status().isForbidden()); + } + @Test public void testUpdate() throws Exception { User updated = new User(USER); @@ -54,6 +71,7 @@ public void testUpdate() throws Exception { updated.setRoles(Collections.singletonList(Role.ROLE_ADMIN)); mockMvc.perform(put(REST_URL + USER_ID) .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) .content(JsonUtil.writeValue(updated))) .andExpect(status().isOk()); @@ -65,6 +83,7 @@ public void testCreate() throws Exception { User expected = new User(null, "New", "new@gmail.com", "newPass", Role.ROLE_USER, Role.ROLE_ADMIN); ResultActions action = mockMvc.perform(post(REST_URL) .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) .content(JsonUtil.writeValue(expected))) .andExpect(status().isCreated()); @@ -77,7 +96,8 @@ public void testCreate() throws Exception { @Test public void testGetAll() throws Exception { - TestUtil.print(mockMvc.perform(get(REST_URL)) + TestUtil.print(mockMvc.perform(get(REST_URL) + .with(userHttpBasic(ADMIN))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(contentJson(ADMIN, USER))); diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java index 3a46d6f6e382..89d90d751bef 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -13,6 +13,7 @@ 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.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; @@ -21,16 +22,24 @@ public class ProfileRestControllerTest extends AbstractControllerTest { @Test public void testGet() throws Exception { TestUtil.print( - mockMvc.perform(get(REST_URL)) + mockMvc.perform(get(REST_URL) + .with(userHttpBasic(USER))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(contentJson(USER)) ); } + @Test + public void testGetUnauth() throws Exception { + mockMvc.perform(get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + @Test public void testDelete() throws Exception { - mockMvc.perform(delete(REST_URL)) + mockMvc.perform(delete(REST_URL) + .with(userHttpBasic(USER))) .andExpect(status().isNoContent()); assertMatch(userService.getAll(), ADMIN); } @@ -40,6 +49,7 @@ public void testUpdate() throws Exception { UserTo updatedTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500); mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER)) .content(JsonUtil.writeValue(updatedTo))) .andDo(print()) .andExpect(status().isOk()); From a94bab044f7ab289e2b79f3915ecfbaee04d3c4e Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Fri, 19 Jan 2018 20:49:25 +0300 Subject: [PATCH 109/139] + --- doc/lesson02.md | 228 +++++------ doc/lesson03.md | 279 +++++++------- doc/lesson04.md | 120 +++--- doc/lesson06.md | 89 +++-- doc/lesson08.md | 83 ++-- doc/lesson09.md | 978 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1388 insertions(+), 389 deletions(-) create mode 100644 doc/lesson09.md diff --git a/doc/lesson02.md b/doc/lesson02.md index 0f394f625e2f..8c0e7732f9f6 100644 --- a/doc/lesson02.md +++ b/doc/lesson02.md @@ -18,8 +18,8 @@ - - + + @@ -36,19 +36,20 @@ - + - + - - + + + - + @@ -63,13 +64,13 @@ - + - + @@ -167,18 +168,18 @@
    -
    +
    @@ -273,7 +274,6 @@
    - @@ -285,21 +285,22 @@ -
    -
    -
    +
    +
    +
    -
    -
    - -

    video 1. Отображения списка еды в JSP

    +

    video 1. Отображения списка еды в JSP

    Apply 2_1_HW1.patch

      @@ -682,74 +683,74 @@
    • Сделал вспомогательный метод getWithExceeded
    -
  • Форматирование даты сделал на основе JSTL LocalDateTime format
  • +
  • Форматирование даты сделал на основе JSTL LocalDateTime format
  • Переименовал TimeUtil в DateTimeUtil
  • Переименовал mealList.jsp в meals.jsp
  • Добавил еще один способ вывести dateTime через стандартную JSTL функцию replace (префикс fn в шапке также надо поменять)
  • -

    video 2. Optional: реализация CRUD

    +

    video 2. Optional: реализация CRUD

    Apply 2_2_HW1_optional.patch

    Про использование паттерна Repository будет подробно рассказано в видео "Слои приложения"

    • Переименовал mealEdit.jsp в mealForm.jsp
    • -
    • В meal.jsp используется параметр запроса param.action, который не кладется в атрибуты.
    • +
    • В meal.jsp используется параметр запроса param.action, который не кладется в атрибуты.
    • В MealServlet.doGet() сделал выбор через switch
    • Пофиксил багу c history.back() в mealForm.jsp для FireFox (коммит формы при Cancel, сделал type="button").

    Дополнительно:

    question Вопросы по HW1

    Зачем в InMemoryMealRepositoryImpl наполнять map с помощью нестатического блока инициализации, а не в конструкторе?

    -

    Разницы нет. Сделал чтобы напомнить вам про эту конструкцию. Малоизвестные особенности Java

    +

    Разницы нет. Сделал чтобы напомнить вам про эту конструкцию. Малоизвестные особенности Java

    Почему InMemoryMealRepositoryImpl не singleton?

    -

    Начиная с Servlet API 2.3 пул сервлетов не создается, создается только один инстанс сервлетов. Те. InMemoryMealRepositoryImpl в нашем случае создается тоже только один раз. Далее все наши классы слоев приложения будут создаваться через Spring, бины которого по умолчанию являются синглтонами (в его контексте).

    +

    Начиная с Servlet API 2.3 пул сервлетов не создается, создается только один инстанс сервлетов. Те. InMemoryMealRepositoryImpl в нашем случае создается тоже только один раз. Далее все наши классы слоев приложения будут создаваться через Spring, бины которого по умолчанию являются синглтонами (в его контексте).

    Objects.requireNonNull в MealServlet.getId(request) если у нас нет id в запросе бросает NPE (NullPointerException). Но оно вылетит и без этого метода. Зачем он нужен и почему мы его не обрабатываем?

    -

    Objects.requireNonNull - это проверка предусловия (будет подробно на 4-м занятии). Означает что в метод пришел неверный аргумент (должен быть не null) и приложение сообщает об ошибке сразу на входе (а не "может быть где-то потом"). См. What is the purpose of Objects#requireNonNull. Если ее проглатывать или замазывать, то приложение возможно где-то работает неверно (приходят неверные аргументы), а мы об этом не узнаем. Красиво обрабатывать ошибки будем на последних занятиях (Spring Exception Handling).

    +

    Objects.requireNonNull - это проверка предусловия (будет подробно на 4-м занятии). Означает что в метод пришел неверный аргумент (должен быть не null) и приложение сообщает об ошибке сразу на входе (а не "может быть где-то потом"). См. What is the purpose of Objects#requireNonNull. Если ее проглатывать или замазывать, то приложение возможно где-то работает неверно (приходят неверные аргументы), а мы об этом не узнаем. Красиво обрабатывать ошибки будем на последних занятиях (Spring Exception Handling).

    Занятие 2:

    -

    video 3. Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava

    +

    video 3. Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava

    -

    video 4. Слои приложения. Создание каркаса приложения.

    +

    video 4. Слои приложения. Создание каркаса приложения.

    Apply 2_3_app_layers.patch

    • Переименовал ExceptionUtil в ValidationUtil
    • Поменял LoggedUser на AuthorizedUser
    • -
    • Добавил проверку id пользователя, пришедшего в контроллер (treat IDs in REST body, "If it is a public API you should be conservative when you reply, but accept liberally")
    • +
    • Добавил проверку id пользователя, пришедшего в контроллер (treat IDs in REST body, "If it is a public API you should be conservative when you reply, but accept liberally")

    Слои приложения

    @@ -768,7 +769,7 @@

    (D)TO может быть как частью одного entity (набор полей) так и набором нескольких entities. В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например MealsWithExceeded мы отдаем наружу и он является Transfer Object, его надо перенести в пакет to. На многих проектах (и собеседованиях) практикуют разделение на уровне maven модулей entity слоя от логики и соответствующей конвертацией ВСЕХ Entity в TO, даже если у них те же самые поля. -Хороший ответ когда TO обязательны есть на stackoverflow: When to Use.

    +Хороший ответ когда TO обязательны есть на stackoverflow: When to Use.

    Почему контроллеры положили в папку web, а не в conrollers?

    @@ -776,7 +777,7 @@

    Зачем мы наследуем NotFoundException от RuntimeException?

    -

    Так с ним удобнее работать. И у нас нет никаких действий по восстановлению состояния приложения (no recoverable conditions): checked vs unchecked exception. По последним данным checked exception вообще depricated: Ignore Checked Exceptions

    +

    Так с ним удобнее работать. И у нас нет никаких действий по восстановлению состояния приложения (no recoverable conditions): checked vs unchecked exception. По последним данным checked exception вообще depricated: Ignore Checked Exceptions

    Зачем в API пишем NotFoundException, если они RuntimeException?

    @@ -793,58 +794,58 @@

    Что лучше возвращать из API: Collection или List

    Вообще, как правило, возвращают List, если не просится по коду более общий случай (например возможный Set или Collection, возвращаемый Map.values()). Если возвращается отсортированный список, то List будет адекватнее.

    -

    video 5. Обзор Spring Framework. Spring Context.

    +

    video 5. Обзор Spring Framework. Spring Context.

    Apply 2_4_add_spring_context.patch

    Apply 2_5_add_dependency_injection.patch

    Apply 2_6_add_annotation_processing.patch

    Apply 2_7_constructor_injection.patch

    В контроллерах Constructor Injection делать не стал, добавляется лишний код (попробуйте сделать сами). На каждом проекте свои правила, универсальных нет.

    Дополнительно видео по Spring

    -

    video 6. Пояснения к HW2. Обработка Autowired

    +

    video 6. Пояснения к HW2. Обработка Autowired

    <context:annotation-config/> говорит спрингу при поднятии контекста обрабатывать @Autowired (добавляется в контекст спринга AutowiredAnnotationBeanPostProcessor). После того, как все бины уже в контексте постпроцессор через отражение инжектит все @Autowired зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке.

    question Ваши вопросы

    Что такое схема в spring-app.xml xsi:schemaLocation и зачем она нужна

    -

    XML схема нужна для валидации xml, IDEA делает по ней автозаполнение.

    +

    XML схема нужна для валидации xml, IDEA делает по ней автозаполнение.

    Что означает для Spring

    @@ -857,14 +858,14 @@ and beans

    Как биндинг происходит для @Autowired? Как поступать, если у нас больше одной реализации UserRepository?

    @Autowired инжектит по типу (т.е. ижектит класс который реализует UserRepository). Обычно он один. Если у нас несколько реализаций, Spring не поднимится и поругается - No unique bean. -В этом случае можно уточнить имя бина через @Qualifier. @Qualifier обычно добавляют только в случае нескольких реализаций.

    +В этом случае можно уточнить имя бина через @Qualifier. @Qualifier обычно добавляют только в случае нескольких реализаций.

    Почему нельзя сервлет помещать в Spring контекст?

    Сервлеты- это исключительно классы servlet-api (веб контейнера) и должны инстанциироваться и работать в нем. Те технически можно ( без init/destroy), но идеологически - неверно. Также НЕ надо работать с cервлетом из SpringMain.


      -
    • Еще раз смотрим на демо приложение и вникаем, что такое пользователь и его еда и что он может с ней сделать. Когда пользователь авторизуется в приложении, его id и норма калорий чудесным образом попадают в AuthorizedUser и в приложении мы может обращаемся к ним. Как они реально туда попадут будет в уроке 9 (Spring Security, сессия и куки)
    • +
    • Еще раз смотрим на демо приложение и вникаем, что такое пользователь и его еда и что он может с ней сделать. Когда пользователь авторизуется в приложении, его id и норма калорий чудесным образом попадают в AuthorizedUser и в приложении мы может обращаемся к ним. Как они реально туда попадут будет в уроке 9 (Spring Security, сессия и куки)
    • Перед началом выполнения ДЗ (ели есть хоть какие-то сомнения) прочитайте ВСЕ ДЗ. Если вопросы остаются- то ВСЕ подсказки. Особенно этот пункт важный, когда будете делать реальное рабочее ТЗ.

    hw Домашнее задание HW02

    @@ -879,14 +880,14 @@ and beans
  • 3.4: атомарность операций не требуется (коллизии при одновременном изменении одного пользователя можно не учитывать)
  • -
  • 4: Реализовать слои приложения для функциональности "еда". API контроллера должна удовлетворять все потребности демо приложения и ничего лишнего (см. демо). +
  • 4: Реализовать слои приложения для функциональности "еда". API контроллера должна удовлетворять все потребности демо приложения и ничего лишнего (см. демо).
    • Смотрите на реализацию слоя для user и делаете по аналогии! Если там что-то непонятно, не надо исправлять или делать по своему. Задавайте вопросы. Если действительно нужна правка- я сделаю и напишу всем.
    • 4.1: после авторизации (сделаем позднее), id авторизованного юзера будет попадать в AuthorizedUser.id(). Запрос попадает в контроллер, методы которого будут доступны снаружи по http, т.е. запрос можно будет сделать с ЛЮБЫМ id для еды (не принадлежащем авторизированному пользователю). Нельзя позволять модифицировать/смотреть чужую еду.
    • 4.2: AuthorizedUser известен только на слое web (см. реализацию ProfileRestController). MealService можно тестировать без подмены логики авторизации, принимаем в методах сервиса и репозитория параметр userId: id владельца еды.
    • 4.3: если еда не принадлежит авторизированному пользователю или отсутствует, в MealServiceImpl бросать NotFoundException.
    • -
    • 4.4: конвертацию в MealWithExceeded можно делать как в слое web, так и в service (Mapping Entity->DTO: Controller or Service?)
    • +
    • 4.4: конвертацию в MealWithExceeded можно делать как в слое web, так и в service (Mapping Entity->DTO: Controller or Service?)
    • 4.5: в MealServiceImpl постараться сделать в каждом методе только одни запрос к MealRepository
    • 4.6 еще раз: не надо в названиях методов повторять названия класса (Meal).
    @@ -900,7 +901,7 @@ and beans
  • 6.1: учесть, что когда будем работать через Spring MVC, MealServlet удалим, те вся логика должна быть в контроллере
  • -
  • 7: добавить в meals.jsp и MealServlet две отдельные фильтрации еды: по дате и по времени (см. демо)
  • +
  • 7: добавить в meals.jsp и MealServlet две отдельные фильтрации еды: по дате и по времени (см. демо)
  • 8: добавить выбор текущего залогиненного пользователя (имитация авторизации, сделать Select с двумя элементами со значениями 1 и 2 в index.html и AuthorizedUser.setId(userId) в UserServlet). Настоящая атворизация будет через Spring Security позже.
  • @@ -959,6 +960,7 @@ and beans
    +
    @@ -972,7 +974,7 @@ and beans @@ -285,21 +285,22 @@ -
    -
    -
    +
    +
    +
    -
    -
    -

    У JUnit есть ассерты и у спринга тоже. Можно ли обойтись без JUnit?

    @@ -885,11 +894,11 @@

    Я так понял VARCHAR быстрее, чем TEXT, когда мы работаем с небольшими записями. Наши записи будут небольшими (255). Почему вы приняли решение перейти на TEXT?

    -

    В отличие от MySql в Postgres VARCHAR и TEXT - тоже самое: http://stackoverflow.com/questions/4848964/postgresql-difference-between-text-and-varchar-character-varying

    +

    В отличие от MySql в Postgres VARCHAR и TEXT - тоже самое: http://stackoverflow.com/questions/4848964/postgresql-difference-between-text-and-varchar-character-varying

    Зачем при создании таблицы мы создаем CREATE UNIQUE INDEX и CREATE INDEX. При каких запросах он будет использоваться?

    -

    UNIQUE индекс нужен для обеcпечения уникальности, DB не даст сделать одинаковый индекс. Индексы используется для скорости выполнения запросов. Обычно они задействуются, когда в запросе есть условия, на которые сделан индекс. Узнать по конкретному запросу можно запросив план запроса: см. Оптимизация запросов. Основы EXPLAIN в PostgreSQL. На измерение производительности с индексами посмотрим в следующем уроке.

    +

    UNIQUE индекс нужен для обеcпечения уникальности, DB не даст сделать одинаковый индекс. Индексы используется для скорости выполнения запросов. Обычно они задействуются, когда в запросе есть условия, на которые сделан индекс. Узнать по конкретному запросу можно запросив план запроса: см. Оптимизация запросов. Основы EXPLAIN в PostgreSQL. На измерение производительности с индексами посмотрим в следующем уроке.

    А это нормально, что у нас в базе у meals есть userId, а в классе - нет?

    @@ -897,11 +906,11 @@

    Почему мы использует один sequence на разные таблицы?

    -

    Мы будем использовать Hibernate, по умолчанию он делает глобальный sequence на все таблицы. В этом подходе есть как плюсы, так и минусы, из плюсов - удобно делать ссылки в коде и в таблицах на при наследовании и мапы в коде. В дополнение: Configure Hibernate to create separate sequence for each table by default.

    +

    Мы будем использовать Hibernate, по умолчанию он делает глобальный sequence на все таблицы. В этом подходе есть как плюсы, так и минусы, из плюсов - удобно делать ссылки в коде и в таблицах на при наследовании и мапы в коде. В дополнение: Configure Hibernate to create separate sequence for each table by default.

    hw Домашнее задание HW03

    • 1 Понять, почему перестали работать SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest
    • -
    • 2 Дополнить скрипты создания и инициализации базы таблицой MEALS. Запустить скрипты на вашу базу (через Run). Порядок таблиц при DROP и DELETE важен, если они связаны fk. Проверьте, что ваши скрипты работают
    • +
    • 2 Дополнить скрипты создания и инициализации базы таблицой MEALS. Запустить скрипты на вашу базу (через Run). Порядок таблиц при DROP и DELETE важен, если они связаны внешними ключами (foreign key, fk). Проверьте, что ваши скрипты работают
    • 3 Реализовать через Spring JDBC Template JdbcMealRepositoryImpl
      • 3.1. сделать каждый метод за один SQL запрос
      • @@ -920,23 +929,23 @@

        По Ctrl+Shift+T (выбрать JUnit4) можно создать тест для конкретного класса, выбрав для него нужные методы. Тестовый класс создастся в папке test в том же пакете, что и тестируемый.

    question Как правильно придумать индекс для базы? Указать в нем все поля, комбинация которых создает по смыслу уникальную запись, или какие-то еще есть условия?

    -

    Индекс нужно делать по тем полям, по которым будут искаться записи (участвуют в WHERE, ORDER BY). Уникальность - совсем не обязательное условие. Индексы ускоряют поиск по определенным полям таблицы. Они не бесплатные (хранятся в памяти, замедляется вставка), поэтому на всякий случай их делать не надо. Также не строят индексы на колонки с малым процентом уникальности (например поле "М/Ж"). Поля индекса НЕ КОММУТАТИВНЫ и порядок полей в описании индекса НЕОБХОДИМО соблюдать (в силу использования B-деревьев и их производных как поисковый механизм индекса). При построении плана запроса EXPLAIN учитывается количество записей в базе, поэтому вместо индексного поиска (Index Scan) база может выбрать последовательный (Seq Scan). Проверить, работают ли индексы можно отключив Seq Scan. Также см. Queries on the first field of composite index

    +

    Индекс нужно делать по тем полям, по которым будут искаться записи (участвуют в WHERE, ORDER BY). Уникальность - совсем не обязательное условие. Индексы ускоряют поиск по определенным полям таблицы. Они не бесплатные (хранятся в памяти, замедляется вставка), поэтому на всякий случай их делать не надо. Также не строят индексы на колонки с малым процентом уникальности (например поле "М/Ж"). Поля индекса НЕ КОММУТАТИВНЫ и порядок полей в описании индекса НЕОБХОДИМО соблюдать (в силу использования B-деревьев и их производных как поисковый механизм индекса). При построении плана запроса EXPLAIN учитывается количество записей в базе, поэтому вместо индексного поиска (Index Scan) база может выбрать последовательный (Seq Scan). Проверить, работают ли индексы можно отключив Seq Scan. Также см. Queries on the first field of composite index

    error Решение проблем

    Из каталога main не видятся классы/ресурсы в test

    @@ -965,6 +974,9 @@
  • 5: В реализации JdbcMealRepositoryImpl одним SQL запросом используйте возвращаемое update значение the number of rows affected
  • 6: При тестировании не портите констант из MealTestData
  • 7: Проверьте, что все, что относится к тестам, ноходится в каталоге test (не попадает в сборку проекта)
  • +
  • 8: Еще раз: в тестах проверять через JUnit Assert нельзя. Он проверяет через equals, который, учитавая будующие JPA реализации, мы не можем переопределить как сравнение по всем поля.
  • +
  • 9: НЕ делайте склейку SQL запросов вручную из параметров, только через jdbcTemplate параметры! См. Внедрение_SQL-кода
  • +
  • 10: Напомню: BeanPropertyRowMapper работает через отражение. Ему нужны геттеры/сеттеры и имена полей должны "совпадать" с колонками ResultSet (Column values are mapped based on matching the column name as obtained from result set metadata to public setters for the corresponding properties. The names are matched either directly or by transforming a name separating the parts with underscores to the same name using "camel" case).
  • @@ -978,6 +990,7 @@
    +
    @@ -991,7 +1004,7 @@ -
    @@ -285,21 +285,22 @@ -
    -
    -
    +
    +
    +
    -
    -
    - @@ -1062,7 +1061,7 @@ Hibernate supports following open-source cache implementations out-of-the-box: E @@ -287,21 +285,22 @@ -
    -
    -
    +
    +
    +
    -
    -
    + +
    + -
    -
    +
    +
    - Permalink + Permalink - +
    @@ -595,15 +594,15 @@
    - - aec72a1 + + 1492258 - Dec 28, 2017 + Jan 11, 2018
    @@ -643,12 +642,12 @@ -
    +
    -
    +
    - 207 lines (153 sloc) + 209 lines (155 sloc) - 19.9 KB + 20.3 KB
    @@ -779,6 +778,7 @@
  • jQuery для всех.
  • jQuery для начинающих. AJAX.
  • jQuery для начинающих. Селекторы.
  • +
  • jQuery task from freecodecamp
  • jQuery API
  • Редактирование таблицы на основе модального окна Bootstrap.
  • Javascript плагины для Bootstrap
  • @@ -920,6 +920,7 @@
  • 3: enable/disable делать c @Transactional (можно реализовать как на уровне репозитория, так и на уровне сервиса через несколько sql, которые должны быть в одной транзакции)
  • 4: в datatablesUtil.js следует выносите только общие скрипты (cкрипты еды размещайте в mealDatatables.js, пользователей в userDatatables.js)
  • 5: если в контроллер приходит null проверте в Network вкладке браузера в каком формате приходят данные и в каком формате в контроллере вы их принимаете (consumes).
  • +
  • 6: при реализации enable/disable лучше явно указывать нужное состояние, чем переключать на противоположное. Если параллельно вам кто-то изменит состояние, то будет несоответствие UI и DB.
  • @@ -947,7 +948,7 @@ @@ -101,7 +84,7 @@ + \ No newline at end of file diff --git a/src/main/webapp/resources/js/mealDatatables.js b/src/main/webapp/resources/js/mealDatatables.js index 3e83a59937b4..15dddd2441f4 100644 --- a/src/main/webapp/resources/js/mealDatatables.js +++ b/src/main/webapp/resources/js/mealDatatables.js @@ -5,7 +5,7 @@ function updateTable() { $.ajax({ type: "POST", url: ajaxUrl + "filter", - data: $("#filter").serialize(), + data: $("#filter").serialize() }).done(updateTableByData); } @@ -16,11 +16,21 @@ function clearFilter() { $(function () { datatableApi = $("#datatable").DataTable({ + "ajax": { + "url": ajaxUrl, + "dataSrc": "" + }, "paging": false, "info": true, "columns": [ { - "data": "dateTime" + "data": "dateTime", + "render": function (date, type, row) { + if (type === 'display') { + return date.replace('T', ' ').substr(0, 16); + } + return date; + } }, { "data": "description" @@ -29,11 +39,13 @@ $(function () { "data": "calories" }, { - "defaultContent": "Edit", + "render": renderEditBtn, + "defaultContent": "", "orderable": false }, { - "defaultContent": "Delete", + "render": renderDeleteBtn, + "defaultContent": "", "orderable": false } ], @@ -42,7 +54,10 @@ $(function () { 0, "desc" ] - ] + ], + "createdRow": function (row, data, dataIndex) { + $(row).addClass(data.exceed ? 'exceeded' : 'normal'); + }, + "initComplete": makeEditable }); - makeEditable(); }); \ No newline at end of file From 0f86c96f93ff5866e4f3015d5081c5eef3a0f725 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Fri, 19 Jan 2018 20:53:16 +0300 Subject: [PATCH 111/139] 10_02 HW09 test --- .../topjava/web/RootControllerTest.java | 6 +-- .../web/meal/MealRestControllerTest.java | 39 +++++++++++++------ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java index 9fed82a03c04..7b147cb48fb5 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -1,8 +1,6 @@ package ru.javawebinar.topjava.web; import org.junit.Test; -import ru.javawebinar.topjava.MealTestData; -import ru.javawebinar.topjava.util.MealsUtil; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @@ -37,8 +35,6 @@ public void testMeals() throws Exception { .with(userAuth(USER))) .andDo(print()) .andExpect(view().name("meals")) - .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp")) - .andExpect(model().attribute("meals", MealsUtil.getWithExceeded(MealTestData.MEALS, USER.getCaloriesPerDay()))); + .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp")); } - } \ 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 index 89213d0bfb84..de57341d99d1 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -18,9 +18,10 @@ 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.MealTestData.assertMatch; import static ru.javawebinar.topjava.TestUtil.contentJson; -import static ru.javawebinar.topjava.TestUtil.contentJsonArray; -import static ru.javawebinar.topjava.UserTestData.USER; +import static ru.javawebinar.topjava.TestUtil.*; +import static ru.javawebinar.topjava.UserTestData.*; import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class MealRestControllerTest extends AbstractControllerTest { @@ -32,16 +33,24 @@ public class MealRestControllerTest extends AbstractControllerTest { @Test public void testGet() throws Exception { - mockMvc.perform(get(REST_URL + MEAL1_ID)) + mockMvc.perform(get(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(ADMIN))) .andExpect(status().isOk()) .andDo(print()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) - .andExpect(contentJson(MEAL1)); + .andExpect(contentJson(ADMIN_MEAL1)); + } + + @Test + public void testGetUnauth() throws Exception { + mockMvc.perform(get(REST_URL + MEAL1_ID)) + .andExpect(status().isUnauthorized()); } @Test public void testDelete() throws Exception { - mockMvc.perform(delete(REST_URL + MEAL1_ID)) + mockMvc.perform(delete(REST_URL + MEAL1_ID) + .with(userHttpBasic(USER))) .andExpect(status().isNoContent()); assertMatch(service.getAll(START_SEQ), MEAL6, MEAL5, MEAL4, MEAL3, MEAL2); } @@ -50,8 +59,10 @@ public void testDelete() throws Exception { public void testUpdate() throws Exception { Meal updated = getUpdated(); - mockMvc.perform(put(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON) - .content(JsonUtil.writeValue(updated))) + mockMvc.perform(put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated)) + .with(userHttpBasic(USER))) .andExpect(status().isOk()); assertMatch(service.get(MEAL1_ID, START_SEQ), updated); @@ -62,18 +73,20 @@ public void testCreate() throws Exception { Meal created = getCreated(); ResultActions action = mockMvc.perform(post(REST_URL) .contentType(MediaType.APPLICATION_JSON) - .content(JsonUtil.writeValue(created))); + .content(JsonUtil.writeValue(created)) + .with(userHttpBasic(ADMIN))); Meal returned = TestUtil.readFromJson(action, Meal.class); created.setId(returned.getId()); assertMatch(returned, created); - assertMatch(service.getAll(START_SEQ), created, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); + assertMatch(service.getAll(ADMIN_ID), ADMIN_MEAL2, created, ADMIN_MEAL1); } @Test public void testGetAll() throws Exception { - mockMvc.perform(get(REST_URL)) + mockMvc.perform(get(REST_URL) + .with(userHttpBasic(USER))) .andExpect(status().isOk()) .andDo(print()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) @@ -84,7 +97,8 @@ public void testGetAll() throws Exception { public void testFilter() throws Exception { mockMvc.perform(get(REST_URL + "filter") .param("startDate", "2015-05-30").param("startTime", "07:00") - .param("endDate", "2015-05-31").param("endTime", "11:00")) + .param("endDate", "2015-05-31").param("endTime", "11:00") + .with(userHttpBasic(USER))) .andExpect(status().isOk()) .andDo(print()) .andExpect(contentJsonArray( @@ -94,7 +108,8 @@ public void testFilter() throws Exception { @Test public void testFilterAll() throws Exception { - mockMvc.perform(get(REST_URL + "filter?startDate=&endTime=")) + mockMvc.perform(get(REST_URL + "filter?startDate=&endTime=") + .with(userHttpBasic(USER))) .andExpect(status().isOk()) .andDo(print()) .andExpect(contentJson(MealsUtil.getWithExceeded(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1), USER.getCaloriesPerDay()))); From 31f4fd522d22cdd435ab380336dc05705b08bd3c Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 21 Jan 2018 19:08:20 +0300 Subject: [PATCH 112/139] 10_03 HW09 datetimepicker --- .../ru/javawebinar/topjava/model/Meal.java | 3 +- .../topjava/util/DateTimeUtil.java | 3 +- .../webapp/WEB-INF/jsp/fragments/headTag.jsp | 2 ++ src/main/webapp/WEB-INF/jsp/meals.jsp | 26 +++++++------- .../webapp/resources/js/datatablesUtil.js | 8 ++++- .../webapp/resources/js/mealDatatables.js | 35 ++++++++++++++++++- 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 919fcd26c726..0880a338a886 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -4,6 +4,7 @@ import org.hibernate.annotations.OnDeleteAction; import org.hibernate.validator.constraints.Range; import org.springframework.format.annotation.DateTimeFormat; +import ru.javawebinar.topjava.util.DateTimeUtil; import javax.persistence.*; import javax.validation.constraints.NotBlank; @@ -31,7 +32,7 @@ public class Meal extends AbstractBaseEntity { @Column(name = "date_time", nullable = false) @NotNull - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) private LocalDateTime dateTime; @Column(name = "description", nullable = false) diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 11269a5ede40..2025cdf2341f 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -8,7 +8,8 @@ import java.time.format.DateTimeFormatter; public class DateTimeUtil { - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm"; + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); public static final LocalDate MIN_DATE = LocalDate.of(1, 1, 1); public static final LocalDate MAX_DATE = LocalDate.of(3000, 1, 1); diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 2d03b845e5e7..2e0e09eda9e3 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -11,6 +11,7 @@ + @@ -19,4 +20,5 @@ + \ 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 index ace4499279a7..8a4e6ce49e1b 100644 --- a/src/main/webapp/WEB-INF/jsp/meals.jsp +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -19,33 +19,33 @@
    - -
    - +
    +
    - -
    - +
    +
    - -
    - +
    +
    - -
    - +
    +
    @@ -95,7 +95,7 @@ code="meal.dateTime"/>
    - ">
    diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js index 9960aef7126e..6a00b8413f6a 100644 --- a/src/main/webapp/resources/js/datatablesUtil.js +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -20,12 +20,18 @@ function updateRow(id) { $("#modalTitle").html(i18n["editTitle"]); $.get(ajaxUrl + id, function (data) { $.each(data, function (key, value) { - form.find("input[name='" + key + "']").val(value); + form.find("input[name='" + key + "']").val( + key === "dateTime" ? formatDate(value) : value + ); }); $('#editRow').modal(); }); } +function formatDate(date) { + return date.replace('T', ' ').substr(0, 16); +} + function deleteRow(id) { $.ajax({ url: ajaxUrl + id, diff --git a/src/main/webapp/resources/js/mealDatatables.js b/src/main/webapp/resources/js/mealDatatables.js index 15dddd2441f4..e77c3deb07e7 100644 --- a/src/main/webapp/resources/js/mealDatatables.js +++ b/src/main/webapp/resources/js/mealDatatables.js @@ -27,7 +27,7 @@ $(function () { "data": "dateTime", "render": function (date, type, row) { if (type === 'display') { - return date.replace('T', ' ').substr(0, 16); + return formatDate(date); } return date; } @@ -60,4 +60,37 @@ $(function () { }, "initComplete": makeEditable }); + +// 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 + }) + } + }); + + $('#startTime, #endTime').datetimepicker({ + datepicker: false, + format: 'H:i' + }); + + $('#dateTime').datetimepicker({ + format: 'Y-m-d H:i' + }); }); \ No newline at end of file From c3292159fd3af2c490a4db584a148c5ccd0f09d4 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 21 Jan 2018 19:17:33 +0300 Subject: [PATCH 113/139] 10_04 jquery converters --- .../webapp/resources/js/datatablesUtil.js | 12 ++++------- .../webapp/resources/js/mealDatatables.js | 21 ++++++++++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js index 6a00b8413f6a..3c6910c36d9b 100644 --- a/src/main/webapp/resources/js/datatablesUtil.js +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -20,18 +20,12 @@ function updateRow(id) { $("#modalTitle").html(i18n["editTitle"]); $.get(ajaxUrl + id, function (data) { $.each(data, function (key, value) { - form.find("input[name='" + key + "']").val( - key === "dateTime" ? formatDate(value) : value - ); + form.find("input[name='" + key + "']").val(value); }); $('#editRow').modal(); }); } -function formatDate(date) { - return date.replace('T', ' ').substr(0, 16); -} - function deleteRow(id) { $.ajax({ url: ajaxUrl + id, @@ -79,8 +73,10 @@ function successNoty(key) { function failNoty(jqXHR) { closeNoty(); + // https://stackoverflow.com/questions/48229776 + var responseJSON = JSON.parse(jqXHR.responseText); failedNote = new Noty({ - text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + (jqXHR.responseJSON ? "
    " + jqXHR.responseJSON : ""), + text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + (responseJSON ? "
    " + responseJSON : ""), type: "error", layout: "bottomRight" }).show(); diff --git a/src/main/webapp/resources/js/mealDatatables.js b/src/main/webapp/resources/js/mealDatatables.js index e77c3deb07e7..17e7a90feab2 100644 --- a/src/main/webapp/resources/js/mealDatatables.js +++ b/src/main/webapp/resources/js/mealDatatables.js @@ -14,6 +14,19 @@ function clearFilter() { $.get(ajaxUrl, 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 () { datatableApi = $("#datatable").DataTable({ "ajax": { @@ -24,13 +37,7 @@ $(function () { "info": true, "columns": [ { - "data": "dateTime", - "render": function (date, type, row) { - if (type === 'display') { - return formatDate(date); - } - return date; - } + "data": "dateTime" }, { "data": "description" From 17e11274f16545f6495b93896baa69eaebd64caa Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 21 Jan 2018 19:24:58 +0300 Subject: [PATCH 114/139] 10_05 persist validate group --- src/main/java/ru/javawebinar/topjava/View.java | 7 +++++++ src/main/java/ru/javawebinar/topjava/model/Meal.java | 2 ++ src/main/resources/spring/spring-db.xml | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 src/main/java/ru/javawebinar/topjava/View.java 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/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 0880a338a886..5b6fb05cde98 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -4,6 +4,7 @@ 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.*; @@ -48,6 +49,7 @@ public class Meal extends AbstractBaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) + @NotNull(groups = View.Persist.class) private User user; public Meal() { diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 0e3fbfbd13e6..0d0ee5071d6a 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -84,6 +84,9 @@ + + + diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java index 142996663d1a..25cf714376ba 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.web; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -11,6 +12,8 @@ public String root() { return "redirect:meals"; } +// @Secured("ROLE_ADMIN") + @PreAuthorize("hasRole('ROLE_ADMIN')") @GetMapping("/users") public String users() { return "users"; diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index f5b51c0a82c6..f16f294da1b1 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -2,14 +2,19 @@ xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns="http://www.springframework.org/schema/beans" + xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> + http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd + http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> + + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index bf7c615f4d74..373dff572732 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -1,5 +1,6 @@ <%@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" %> diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java index d18a1fae368e..30bc5919ead0 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -4,7 +4,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; -import ru.javawebinar.topjava.TestUtil; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.service.MealService; import ru.javawebinar.topjava.util.MealsUtil; @@ -91,7 +90,7 @@ public void testCreate() throws Exception { .content(JsonUtil.writeValue(created)) .with(userHttpBasic(ADMIN))); - Meal returned = TestUtil.readFromJson(action, Meal.class); + Meal returned = readFromJson(action, Meal.class); created.setId(returned.getId()); assertMatch(returned, created); diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index b8f1c84a6861..dcad2666a197 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -4,7 +4,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; import ru.javawebinar.topjava.TestUtil; -import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.web.AbstractControllerTest; @@ -101,7 +100,7 @@ public void testCreate() throws Exception { ResultActions action = mockMvc.perform(post(REST_URL) .contentType(MediaType.APPLICATION_JSON) .with(userHttpBasic(ADMIN)) - .content(UserTestData.jsonWithPassword(expected, "newPass"))) + .content(jsonWithPassword(expected, "newPass"))) .andExpect(status().isCreated()); User returned = TestUtil.readFromJson(action, User.class); From d283f1537a07d495ea28d979871adb6ad2dcab5f Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:39:53 +0300 Subject: [PATCH 126/139] 11_01 HW10 fix encoding --- src/main/webapp/WEB-INF/web.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index c471a447ca14..f40dd731cc78 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -37,16 +37,6 @@ / - - - springSecurityFilterChain - org.springframework.web.filter.DelegatingFilterProxy - - - springSecurityFilterChain - /* - - encodingFilter org.springframework.web.filter.CharacterEncodingFilter @@ -64,4 +54,14 @@ /* + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + springSecurityFilterChain + /* + + From 16cbfdde33ec40e003805a4835f2e57f0b675a6d Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:40:18 +0300 Subject: [PATCH 127/139] 11_02 HW10 validation --- config/curl.md | 6 +++- pom.xml | 7 ++++ .../topjava/util/ValidationUtil.java | 18 ----------- .../topjava/util/exception/ErrorInfo.java | 6 ++-- .../topjava/util/exception/ErrorType.java | 5 +-- .../topjava/web/ExceptionInfoHandler.java | 32 ++++++++++++++++--- .../topjava/web/meal/MealAjaxController.java | 11 +------ .../topjava/web/meal/MealRestController.java | 5 +-- .../topjava/web/user/AdminAjaxController.java | 11 +------ .../topjava/web/user/AdminRestController.java | 5 +-- .../web/user/ProfileRestController.java | 4 ++- .../webapp/resources/js/datatablesUtil.js | 2 +- .../web/meal/MealRestControllerTest.java | 30 +++++++++++++++-- .../web/user/AdminRestControllerTest.java | 32 +++++++++++++++++-- .../web/user/ProfileRestControllerTest.java | 17 ++++++++-- 15 files changed, 129 insertions(+), 62 deletions(-) diff --git a/config/curl.md b/config/curl.md index 8e197ec254bb..94127472ca58 100644 --- a/config/curl.md +++ b/config/curl.md @@ -26,4 +26,8 @@ `curl -s -X POST -d '{"dateTime":"2015-06-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` #### update Meals -`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` \ No newline at end of file +`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password`#### update Meals + +#### validate with Error +`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00"}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` +`curl -s -X POST -d '{}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 25dab45b85d2..336eee124d7e 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ 4.12 1.3 1.5.0 + 2.4.0 5.2.12.Final @@ -338,6 +339,12 @@ ${jsonassert.version} test + + com.jayway.jsonpath + json-path + ${json-path.version} + test + org.springframework diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 7507a9530dbd..0e71260a5240 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -1,13 +1,8 @@ package ru.javawebinar.topjava.util; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; import ru.javawebinar.topjava.HasId; import ru.javawebinar.topjava.util.exception.NotFoundException; -import java.util.StringJoiner; - public class ValidationUtil { private ValidationUtil() { @@ -57,17 +52,4 @@ public static Throwable getRootCause(Throwable t) { } return result; } - - public static ResponseEntity getErrorResponse(BindingResult result) { - StringJoiner joiner = new StringJoiner("
    "); - result.getFieldErrors().forEach( - fe -> { - String msg = fe.getDefaultMessage(); - if (!msg.startsWith(fe.getField())) { - msg = fe.getField() + ' ' + msg; - } - joiner.add(msg); - }); - return new ResponseEntity<>(joiner.toString(), HttpStatus.UNPROCESSABLE_ENTITY); - } } \ 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 index d43323590d95..0c5e15a94910 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -3,11 +3,11 @@ public class ErrorInfo { private final String url; private final ErrorType type; - private final String detail; + private final String[] details; - public ErrorInfo(CharSequence url, ErrorType type, String detail) { + public ErrorInfo(CharSequence url, ErrorType type, String... details) { this.url = url.toString(); this.type = type; - this.detail = detail; + this.details = details; } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java index 700a432d3b41..0801d939460b 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java @@ -3,5 +3,6 @@ public enum ErrorType { APP_ERROR, DATA_NOT_FOUND, - DATA_ERROR -} + DATA_ERROR, + VALIDATION_ERROR +} \ 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 index b6380f7520ab..f902f371e5ce 100644 --- a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -6,6 +6,9 @@ import org.springframework.core.annotation.Order; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @@ -17,6 +20,8 @@ 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 { @@ -26,28 +31,45 @@ public class ExceptionInfoHandler { @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) @ExceptionHandler(NotFoundException.class) public ErrorInfo handleError(HttpServletRequest req, NotFoundException e) { - return logAndGetErrorInfo(req, e, false, ErrorType.DATA_NOT_FOUND); + 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, ErrorType.DATA_ERROR); + return logAndGetErrorInfo(req, e, true, DATA_ERROR); + } + + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class}) + public ErrorInfo bindValidationError(HttpServletRequest req, Exception e) { + BindingResult result = e instanceof BindException ? + ((BindException) e).getBindingResult() : ((MethodArgumentNotValidException) e).getBindingResult(); + + String[] details = result.getFieldErrors().stream() + .map(fe -> { + String msg = fe.getDefaultMessage(); + return (msg.startsWith(fe.getField())) ? msg : fe.getField() + ' ' + msg; + }) + .toArray(String[]::new); + + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, details); } @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public ErrorInfo handleError(HttpServletRequest req, Exception e) { - return logAndGetErrorInfo(req, e, true, ErrorType.APP_ERROR); + return logAndGetErrorInfo(req, e, true, APP_ERROR); } - private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType) { + private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType, String... details) { 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()); + return new ErrorInfo(req.getRequestURL(), errorType, + details.length != 0 ? details : new String[]{rootCause.toString()}); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java index 088c6d6a159b..db1448fd4164 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java @@ -1,13 +1,9 @@ package ru.javawebinar.topjava.web.meal; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealWithExceed; -import ru.javawebinar.topjava.util.ValidationUtil; import javax.validation.Valid; import java.time.LocalDate; @@ -37,17 +33,12 @@ public void delete(@PathVariable("id") int id) { } @PostMapping - public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult result) { - if (result.hasErrors()) { - // TODO change to exception handler - return ValidationUtil.getErrorResponse(result); - } + public void createOrUpdate(@Valid Meal meal) { if (meal.isNew()) { super.create(meal); } else { super.update(meal, meal.getId()); } - return new ResponseEntity<>(HttpStatus.OK); } @Override diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index c395cbb5787d..396ed1a7a0f0 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -8,6 +8,7 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealWithExceed; +import javax.validation.Valid; import java.net.URI; import java.time.LocalDate; import java.time.LocalTime; @@ -39,12 +40,12 @@ public List getAll() { @Override @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) - public void update(@RequestBody Meal meal, @PathVariable("id") int id) { + public void update(@Valid @RequestBody Meal meal, @PathVariable("id") int id) { super.update(meal, id); } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWithLocation(@RequestBody Meal meal) { + public ResponseEntity createWithLocation(@Valid @RequestBody Meal meal) { Meal created = super.create(meal); URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java index b3f75f43caf3..eeb67e9407c0 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java @@ -1,14 +1,10 @@ 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.UserUtil; -import ru.javawebinar.topjava.util.ValidationUtil; import javax.validation.Valid; import java.util.List; @@ -36,17 +32,12 @@ public void delete(@PathVariable("id") int id) { } @PostMapping - public ResponseEntity createOrUpdate(@Valid UserTo userTo, BindingResult result) { - if (result.hasErrors()) { - // TODO change to exception handler - return ValidationUtil.getErrorResponse(result); - } + public void createOrUpdate(@Valid UserTo userTo) { if (userTo.isNew()) { super.create(UserUtil.createNewFromTo(userTo)); } else { super.update(userTo, userTo.getId()); } - return new ResponseEntity<>(HttpStatus.OK); } @Override diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index fe869a482bf5..4a6d1247d0bb 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -7,6 +7,7 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import ru.javawebinar.topjava.model.User; +import javax.validation.Valid; import java.net.URI; import java.util.List; @@ -28,7 +29,7 @@ public User get(@PathVariable("id") int id) { } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWithLocation(@RequestBody User user) { + public ResponseEntity createWithLocation(@Valid @RequestBody User user) { User created = super.create(user); // HttpHeaders httpHeaders = new HttpHeaders(); @@ -50,7 +51,7 @@ public void delete(@PathVariable("id") int id) { @Override @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) - public void update(@RequestBody User user, @PathVariable("id") int id) { + public void update(@Valid @RequestBody User user, @PathVariable("id") int id) { super.update(user, id); } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index 629019ddb517..ef253bae38a1 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -7,6 +7,8 @@ import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; +import javax.validation.Valid; + @RestController @RequestMapping(ProfileRestController.REST_URL) public class ProfileRestController extends AbstractUserController { @@ -24,7 +26,7 @@ public void delete() { } @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public void update(@RequestBody UserTo userTo) { + public void update(@Valid @RequestBody UserTo userTo) { super.update(userTo, AuthorizedUser.id()); } diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js index 6ea2fc6d0b0d..9d588d347bba 100644 --- a/src/main/webapp/resources/js/datatablesUtil.js +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -82,7 +82,7 @@ function failNoty(jqXHR) { // https://stackoverflow.com/questions/48229776 var errorInfo = JSON.parse(jqXHR.responseText); failedNote = new Noty({ - text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + "
    " + errorInfo.type + "
    " + errorInfo.detail, + text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + "
    " + errorInfo.type + "
    " + errorInfo.details.join("
    "), type: "error", layout: "bottomRight" }).show(); diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java index 30bc5919ead0..6dd19059e355 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -7,6 +7,7 @@ import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.service.MealService; import ru.javawebinar.topjava.util.MealsUtil; +import ru.javawebinar.topjava.util.exception.ErrorType; import ru.javawebinar.topjava.web.AbstractControllerTest; import ru.javawebinar.topjava.web.json.JsonUtil; @@ -14,8 +15,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.MealTestData.assertMatch; import static ru.javawebinar.topjava.TestUtil.contentJson; @@ -128,4 +128,30 @@ public void testFilterAll() throws Exception { .andDo(print()) .andExpect(contentJson(MealsUtil.getWithExceeded(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1), USER.getCaloriesPerDay()))); } + + @Test + public void testCreateInvalid() throws Exception { + Meal invalid = new Meal(null, null, "Dummy", 200); + mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andDo(print()); + } + + @Test + public void testUpdateInvalid() throws Exception { + Meal invalid = new Meal(MEAL1_ID, null, null, 6000); + mockMvc.perform(put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andDo(print()); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index dcad2666a197..37fdb467afda 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -6,6 +6,7 @@ import ru.javawebinar.topjava.TestUtil; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.util.exception.ErrorType; import ru.javawebinar.topjava.web.AbstractControllerTest; import ru.javawebinar.topjava.web.json.JsonUtil; @@ -13,8 +14,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static ru.javawebinar.topjava.TestUtil.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; @@ -88,7 +88,7 @@ public void testUpdate() throws Exception { mockMvc.perform(put(REST_URL + USER_ID) .contentType(MediaType.APPLICATION_JSON) .with(userHttpBasic(ADMIN)) - .content(JsonUtil.writeValue(updated))) + .content(jsonWithPassword(updated, USER.getPassword()))) .andExpect(status().isOk()); assertMatch(userService.get(USER_ID), updated); @@ -118,4 +118,30 @@ public void testGetAll() throws Exception { .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(contentJson(ADMIN, USER))); } + + @Test + public void testCreateInvalid() throws Exception { + User expected = new User(null, null, "", "newPass", 7300, Role.ROLE_USER, Role.ROLE_ADMIN); + ResultActions action = mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(JsonUtil.writeValue(expected))) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andDo(print()); + } + + @Test + public void testUpdateInvalid() throws Exception { + User updated = new User(USER); + updated.setName(""); + mockMvc.perform(put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(JsonUtil.writeValue(updated))) + .andExpect(status().isUnprocessableEntity()) + .andDo(print()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andDo(print()); + } } \ 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 index 89d90d751bef..0d6d6208bb7a 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -6,13 +6,13 @@ import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; import ru.javawebinar.topjava.util.UserUtil; +import ru.javawebinar.topjava.util.exception.ErrorType; import ru.javawebinar.topjava.web.AbstractControllerTest; import ru.javawebinar.topjava.web.json.JsonUtil; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static ru.javawebinar.topjava.TestUtil.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; @@ -56,4 +56,17 @@ public void testUpdate() throws Exception { assertMatch(userService.getByEmail("newemail@ya.ru"), UserUtil.updateFromTo(new User(USER), updatedTo)); } + + @Test + public void testUpdateInvalid() throws Exception { + UserTo updatedTo = new UserTo(null, null, "password", null, 1500); + + mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER)) + .content(JsonUtil.writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andDo(print()); + } } \ No newline at end of file From 1d9d97517fdb5cdbc864c014955fb474279dc35c Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:46:38 +0300 Subject: [PATCH 128/139] 11_03 HW10 duplicate email --- config/messages/app.properties | 4 +++- config/messages/app_ru.properties | 4 +++- .../topjava/web/RootController.java | 14 ++++++++++++-- .../web/user/AbstractUserController.java | 2 ++ .../topjava/web/user/AdminAjaxController.java | 19 +++++++++++++++---- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/config/messages/app.properties b/config/messages/app.properties index e877dbd197f8..e261a247e11d 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -34,4 +34,6 @@ common.saved=Record saved common.enabled=Record enabled common.disabled=Record disabled common.errorStatus=Error status -common.appError=Application error \ No newline at end of file +common.appError=Application error + +exception.user.duplicateEmail=User with this email already exists \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index cc74871f778b..9234570ab81e 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -34,4 +34,6 @@ common.saved=\u0417\u0430\u043F\u0438\u0441\u044C \u0441\u043E\u0445\u0440\u0430 common.enabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430 common.disabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0434\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430 common.errorStatus=\u0421\u0442\u0430\u0442\u0443\u0441 \u043E\u0448\u0438\u0431\u043A\u0438 -common.appError=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \ No newline at end of file +common.appError=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F + +exception.user.duplicateEmail=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0441 \u0442\u0430\u043A\u043E\u0439 \u043F\u043E\u0447\u0442\u043E\u0439 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0432 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java index 77c943029a9a..81aab8daacb7 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -1,5 +1,6 @@ package ru.javawebinar.topjava.web; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -48,11 +49,15 @@ public String profile() { public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status) { if (result.hasErrors()) { return "profile"; - } else { + } + try { super.update(userTo, AuthorizedUser.id()); AuthorizedUser.get().update(userTo); status.setComplete(); return "redirect:meals"; + } catch (DataIntegrityViolationException ex) { + result.rejectValue("email", EXCEPTION_DUPLICATE_EMAIL); + return "profile"; } } @@ -68,10 +73,15 @@ public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionSt if (result.hasErrors()) { model.addAttribute("register", true); return "profile"; - } else { + } + try { super.create(UserUtil.createNewFromTo(userTo)); status.setComplete(); return "redirect:login?message=app.registered&username=" + userTo.getEmail(); + } catch (DataIntegrityViolationException ex) { + result.rejectValue("email", EXCEPTION_DUPLICATE_EMAIL); + model.addAttribute("register", true); + return "profile"; } } } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index f6bd95465fe2..9eeb4f5d1dc8 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -15,6 +15,8 @@ public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); + public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; + @Autowired private UserService service; diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java index eeb67e9407c0..e929ae0ebc20 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java @@ -1,5 +1,9 @@ package ru.javawebinar.topjava.web.user; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.model.User; @@ -13,6 +17,9 @@ @RequestMapping("/ajax/admin/users") public class AdminAjaxController extends AbstractUserController { + @Autowired + private MessageSource messageSource; + @Override @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List getAll() { @@ -33,10 +40,14 @@ public void delete(@PathVariable("id") int id) { @PostMapping public void createOrUpdate(@Valid UserTo userTo) { - if (userTo.isNew()) { - super.create(UserUtil.createNewFromTo(userTo)); - } else { - super.update(userTo, userTo.getId()); + try { + if (userTo.isNew()) { + super.create(UserUtil.createNewFromTo(userTo)); + } else { + super.update(userTo, userTo.getId()); + } + } catch (DataIntegrityViolationException e) { + throw new DataIntegrityViolationException(messageSource.getMessage(EXCEPTION_DUPLICATE_EMAIL, null, LocaleContextHolder.getLocale())); } } From 4408584ea4ad1c81c5c7f6b68e8d12e5e44ae960 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:47:06 +0300 Subject: [PATCH 129/139] 11_04 HW10 duplicate datetime --- .travis.yml | 4 ++ config/messages/app.properties | 3 +- config/messages/app_ru.properties | 3 +- .../topjava/web/ExceptionInfoHandler.java | 29 ++++++++++++++ .../javawebinar/topjava/web/MessageUtil.java | 27 +++++++++++++ .../topjava/web/RootController.java | 2 + .../web/user/AbstractUserController.java | 2 - .../topjava/web/user/AdminAjaxController.java | 19 ++------- .../topjava/web/AbstractControllerTest.java | 19 +++++++++ .../web/meal/MealRestControllerTest.java | 39 +++++++++++++++++-- .../web/user/AdminRestControllerTest.java | 39 +++++++++++++++++-- .../web/user/ProfileRestControllerTest.java | 22 ++++++++++- 12 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/web/MessageUtil.java diff --git a/.travis.yml b/.travis.yml index 65c75d2dd58f..27f07bc9297b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,10 @@ branches: only: - master +#https://docs.travis-ci.com/user/environment-variables#Default-Environment-Variables +env: +- TOPJAVA_ROOT=$TRAVIS_BUILD_DIR + # https://docs.travis-ci.com/user/notifications#Configuring-email-notifications #notifications: # email: false \ No newline at end of file diff --git a/config/messages/app.properties b/config/messages/app.properties index e261a247e11d..7474f0538714 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -36,4 +36,5 @@ common.disabled=Record disabled common.errorStatus=Error status common.appError=Application error -exception.user.duplicateEmail=User with this email already exists \ No newline at end of file +exception.user.duplicateEmail=User with this email already exists +exception.meal.duplicateDateTime=You already have meal with this date/time \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 9234570ab81e..afce70443613 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -36,4 +36,5 @@ common.disabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0434\u0435\u0430\u043A\u0 common.errorStatus=\u0421\u0442\u0430\u0442\u0443\u0441 \u043E\u0448\u0438\u0431\u043A\u0438 common.appError=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F -exception.user.duplicateEmail=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0441 \u0442\u0430\u043A\u043E\u0439 \u043F\u043E\u0447\u0442\u043E\u0439 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0432 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \ No newline at end of file +exception.user.duplicateEmail=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0441 \u0442\u0430\u043A\u043E\u0439 \u043F\u043E\u0447\u0442\u043E\u0439 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0432 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 +exception.meal.duplicateDateTime=\u0423 \u0432\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0435\u0434\u0430 \u0441 \u0442\u0430\u043A\u043E\u0439 \u0434\u0430\u0442\u043E\u0439/\u0432\u0440\u0435\u043C\u0435\u043D\u0435\u043C \ 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 index f902f371e5ce..7078173057ae 100644 --- a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.dao.DataIntegrityViolationException; @@ -19,6 +20,10 @@ import ru.javawebinar.topjava.util.exception.NotFoundException; import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import static ru.javawebinar.topjava.util.exception.ErrorType.*; @@ -27,6 +32,20 @@ public class ExceptionInfoHandler { private static Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); + public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; + public static final String EXCEPTION_DUPLICATE_DATETIME = "exception.meal.duplicateDateTime"; + + private static final Map CONSTRAINS_I18N_MAP = Collections.unmodifiableMap( + new HashMap() { + { + put("users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL); + put("meals_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME); + } + }); + + @Autowired + private MessageUtil messageUtil; + // http://stackoverflow.com/a/22358422/548473 @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) @ExceptionHandler(NotFoundException.class) @@ -37,6 +56,16 @@ public ErrorInfo handleError(HttpServletRequest req, NotFoundException e) { @ResponseStatus(value = HttpStatus.CONFLICT) // 409 @ExceptionHandler(DataIntegrityViolationException.class) public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { + String rootMsg = ValidationUtil.getRootCause(e).getMessage(); + if (rootMsg != null) { + String lowerCaseMsg = rootMsg.toLowerCase(); + Optional> entry = CONSTRAINS_I18N_MAP.entrySet().stream() + .filter(it -> lowerCaseMsg.contains(it.getKey())) + .findAny(); + if (entry.isPresent()) { + return logAndGetErrorInfo(req, e, false, DATA_ERROR, messageUtil.getMessage(entry.get().getValue())); + } + } return logAndGetErrorInfo(req, e, true, DATA_ERROR); } diff --git a/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java new file mode 100644 index 000000000000..d4f2add25f95 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java @@ -0,0 +1,27 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Locale; + +@Component +public class MessageUtil { + + private final MessageSource messageSource; + + @Autowired + public MessageUtil(MessageSource messageSource) { + this.messageSource = messageSource; + } + + public String getMessage(String code, Locale locale) { + return messageSource.getMessage(code, null, locale); + } + + public String getMessage(String code) { + return getMessage(code, LocaleContextHolder.getLocale()); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java index 81aab8daacb7..aedcdaa90e4e 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -15,6 +15,8 @@ import javax.validation.Valid; +import static ru.javawebinar.topjava.web.ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL; + @Controller public class RootController extends AbstractUserController { diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index 9eeb4f5d1dc8..f6bd95465fe2 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -15,8 +15,6 @@ public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); - public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; - @Autowired private UserService service; diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java index e929ae0ebc20..eeb67e9407c0 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java @@ -1,9 +1,5 @@ package ru.javawebinar.topjava.web.user; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.MessageSource; -import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.model.User; @@ -17,9 +13,6 @@ @RequestMapping("/ajax/admin/users") public class AdminAjaxController extends AbstractUserController { - @Autowired - private MessageSource messageSource; - @Override @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) public List getAll() { @@ -40,14 +33,10 @@ public void delete(@PathVariable("id") int id) { @PostMapping public void createOrUpdate(@Valid UserTo userTo) { - try { - if (userTo.isNew()) { - super.create(UserUtil.createNewFromTo(userTo)); - } else { - super.update(userTo, userTo.getId()); - } - } catch (DataIntegrityViolationException e) { - throw new DataIntegrityViolationException(messageSource.getMessage(EXCEPTION_DUPLICATE_EMAIL, null, LocaleContextHolder.getLocale())); + if (userTo.isNew()) { + super.create(UserUtil.createNewFromTo(userTo)); + } else { + super.update(userTo, userTo.getId()); } } diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java index d881384f4b80..111436bb3d6a 100644 --- a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -9,6 +9,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.WebApplicationContext; @@ -16,10 +17,13 @@ import ru.javawebinar.topjava.AllActiveProfileResolver; import ru.javawebinar.topjava.repository.JpaUtil; import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.util.exception.ErrorType; import javax.annotation.PostConstruct; +import java.util.Locale; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @ContextConfiguration({ "classpath:spring/spring-app.xml", @@ -53,6 +57,9 @@ abstract public class AbstractControllerTest { @Autowired private WebApplicationContext webApplicationContext; + @Autowired + protected MessageUtil messageUtil; + @PostConstruct private void postConstruct() { mockMvc = MockMvcBuilders @@ -69,4 +76,16 @@ public void setUp() { jpaUtil.clear2ndLevelHibernateCache(); } } + + protected String getMessage(String code) { + return messageUtil.getMessage(code, Locale.ENGLISH); + } + + public ResultMatcher errorType(ErrorType type) { + return jsonPath("$.type").value(type.name()); + } + + public ResultMatcher jsonMessage(String path, String code) { + return jsonPath(path).value(getMessage(code)); + } } diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java index 6dd19059e355..182991a2643f 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -4,6 +4,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.service.MealService; import ru.javawebinar.topjava.util.MealsUtil; @@ -15,13 +17,15 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +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.MealTestData.assertMatch; import static ru.javawebinar.topjava.TestUtil.contentJson; import static ru.javawebinar.topjava.TestUtil.*; import static ru.javawebinar.topjava.UserTestData.*; import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; +import static ru.javawebinar.topjava.web.ExceptionInfoHandler.EXCEPTION_DUPLICATE_DATETIME; public class MealRestControllerTest extends AbstractControllerTest { @@ -138,7 +142,7 @@ public void testCreateInvalid() throws Exception { .with(userHttpBasic(ADMIN))) .andDo(print()) .andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andExpect(errorType(ErrorType.VALIDATION_ERROR)) .andDo(print()); } @@ -151,7 +155,36 @@ public void testUpdateInvalid() throws Exception { .with(userHttpBasic(USER))) .andDo(print()) .andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andExpect(errorType(ErrorType.VALIDATION_ERROR)) .andDo(print()); } + + @Test + @Transactional(propagation = Propagation.NEVER) + public void testUpdateDuplicate() throws Exception { + Meal invalid = new Meal(MEAL1_ID, MEAL2.getDateTime(), "Dummy", 200); + + mockMvc.perform(put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isConflict()) + .andExpect(errorType(ErrorType.DATA_ERROR)) + .andExpect(jsonMessage("$.details", EXCEPTION_DUPLICATE_DATETIME)); + } + + @Test + @Transactional(propagation = Propagation.NEVER) + public void testCreateDuplicate() throws Exception { + Meal invalid = new Meal(null, ADMIN_MEAL1.getDateTime(), "Dummy", 200); + mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isConflict()) + .andExpect(errorType(ErrorType.DATA_ERROR)) + .andExpect(jsonMessage("$.details", EXCEPTION_DUPLICATE_DATETIME)); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index 37fdb467afda..e4c3bdf699f4 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -3,6 +3,8 @@ import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.TestUtil; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; @@ -14,9 +16,11 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +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.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.web.ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL; public class AdminRestControllerTest extends AbstractControllerTest { @@ -127,7 +131,7 @@ public void testCreateInvalid() throws Exception { .with(userHttpBasic(ADMIN)) .content(JsonUtil.writeValue(expected))) .andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andExpect(errorType(ErrorType.VALIDATION_ERROR)) .andDo(print()); } @@ -141,7 +145,36 @@ public void testUpdateInvalid() throws Exception { .content(JsonUtil.writeValue(updated))) .andExpect(status().isUnprocessableEntity()) .andDo(print()) - .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andExpect(errorType(ErrorType.VALIDATION_ERROR)) .andDo(print()); } + + @Test + @Transactional(propagation = Propagation.NEVER) + public void testUpdateDuplicate() throws Exception { + User updated = new User(USER); + updated.setEmail("admin@gmail.com"); + mockMvc.perform(put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(jsonWithPassword(updated, "password"))) + .andExpect(status().isConflict()) + .andExpect(errorType(ErrorType.DATA_ERROR)) + .andExpect(jsonMessage("$.details", EXCEPTION_DUPLICATE_EMAIL)) + .andDo(print()); + } + + @Test + @Transactional(propagation = Propagation.NEVER) + public void testCreateDuplicate() throws Exception { + User expected = new User(null, "New", "user@yandex.ru", "newPass", 2300, Role.ROLE_USER, Role.ROLE_ADMIN); + mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(ADMIN)) + .content(jsonWithPassword(expected, "newPass"))) + .andExpect(status().isConflict()) + .andExpect(errorType(ErrorType.DATA_ERROR)) + .andExpect(jsonMessage("$.details", EXCEPTION_DUPLICATE_EMAIL)); + + } } \ 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 index 0d6d6208bb7a..239f172cb8d4 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -2,6 +2,8 @@ import org.junit.Test; import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.TestUtil; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; @@ -12,9 +14,11 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +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.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.web.ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL; import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; public class ProfileRestControllerTest extends AbstractControllerTest { @@ -66,7 +70,21 @@ public void testUpdateInvalid() throws Exception { .content(JsonUtil.writeValue(updatedTo))) .andDo(print()) .andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.type").value(ErrorType.VALIDATION_ERROR.name())) + .andExpect(errorType(ErrorType.VALIDATION_ERROR)) + .andDo(print()); + } + + @Test + @Transactional(propagation = Propagation.NEVER) + public void testDuplicate() throws Exception { + UserTo updatedTo = new UserTo(null, "newName", "admin@gmail.com", "newPassword", 1500); + + mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER)) + .content(JsonUtil.writeValue(updatedTo))) + .andExpect(status().isConflict()) + .andExpect(errorType(ErrorType.DATA_ERROR)) + .andExpect(jsonMessage("$.details", EXCEPTION_DUPLICATE_EMAIL)) .andDo(print()); } } \ No newline at end of file From ee06cc212ad710abaeb1a7526f3d27259c6a82d5 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:47:28 +0300 Subject: [PATCH 130/139] 11_05 HW10 change locale --- src/main/resources/spring/spring-mvc.xml | 8 +++ .../WEB-INF/jsp/fragments/bodyHeader.jsp | 51 ++++++++++++++----- src/main/webapp/WEB-INF/jsp/login.jsp | 19 +------ .../webapp/resources/js/mealDatatables.js | 2 + .../topjava/web/AbstractControllerTest.java | 3 +- 5 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 1fc18104664c..cd06762108d5 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -70,7 +70,15 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index f472a71270ca..95acfadc4033 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -6,19 +6,46 @@ \ 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 index 3d70bcb0be4d..90414000708a 100644 --- a/src/main/webapp/WEB-INF/jsp/login.jsp +++ b/src/main/webapp/WEB-INF/jsp/login.jsp @@ -6,24 +6,7 @@ - +
    diff --git a/src/main/webapp/resources/js/mealDatatables.js b/src/main/webapp/resources/js/mealDatatables.js index 17e7a90feab2..e9ced4efa01d 100644 --- a/src/main/webapp/resources/js/mealDatatables.js +++ b/src/main/webapp/resources/js/mealDatatables.js @@ -68,6 +68,8 @@ $(function () { "initComplete": makeEditable }); + $.datetimepicker.setLocale(localeCode); + // http://xdsoft.net/jqplugins/datetimepicker/ var startDate = $('#startDate'); var endDate = $('#endDate'); diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java index 111436bb3d6a..ad027ef6f6a1 100644 --- a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -20,7 +20,6 @@ import ru.javawebinar.topjava.util.exception.ErrorType; import javax.annotation.PostConstruct; -import java.util.Locale; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -78,7 +77,7 @@ public void setUp() { } protected String getMessage(String code) { - return messageUtil.getMessage(code, Locale.ENGLISH); + return messageUtil.getMessage(code); } public ResultMatcher errorType(ErrorType type) { From a60fe3c1f1fc14bb31198c2f3350097ab5767760 Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:47:48 +0300 Subject: [PATCH 131/139] 11_06 i18n --- config/messages/app.properties | 23 ++++++++++++++++--- config/messages/app_ru.properties | 23 ++++++++++++++++--- .../topjava/util/exception/ErrorInfo.java | 4 +++- .../topjava/util/exception/ErrorType.java | 18 +++++++++++---- .../topjava/web/ExceptionInfoHandler.java | 8 +++---- .../web/GlobalControllerExceptionHandler.java | 10 ++++++-- .../javawebinar/topjava/web/MessageUtil.java | 5 ++++ .../WEB-INF/jsp/exception/exception.jsp | 2 +- .../webapp/WEB-INF/jsp/fragments/i18n.jsp | 2 +- .../webapp/resources/js/datatablesUtil.js | 21 ++++++++++++++++- .../webapp/resources/js/mealDatatables.js | 13 +++-------- .../webapp/resources/js/userDatatables.js | 13 +++-------- 12 files changed, 101 insertions(+), 41 deletions(-) diff --git a/config/messages/app.properties b/config/messages/app.properties index 7474f0538714..dd48a296ab40 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -16,6 +16,11 @@ user.registered=Registered user.password=Password user.caloriesPerDay=Daily calorie limit +userTo.name=Name +userTo.email=Email +userTo.password=Password +userTo.caloriesPerDay=Daily calorie limit + meal.title=Meals meal.edit=Edit meal meal.add=Add meal @@ -33,8 +38,20 @@ 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.search=Search + +error.appError=Application error +error.dataNotFound=Data not found +error.dataError=Data error +error.validationError=Validation error exception.user.duplicateEmail=User with this email already exists -exception.meal.duplicateDateTime=You already have meal with this date/time \ No newline at end of file +exception.meal.duplicateDateTime=You already have meal with this date/time + +NotEmpty="{0}" must not be empty +NotBlank="{0}" must not be empty +NotNull="{0}" must not be empty +Email= Invalid format of "{0}" +Range="{0}" must be between {2} and {1} +Length="{0}" length must be between {2} and {1} +Size="{0}" size must be between {2} and {1} \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index afce70443613..1a136adbcd52 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -16,6 +16,11 @@ user.registered=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u04 user.password=\u041F\u0430\u0440\u043E\u043B\u044C user.caloriesPerDay=\u041D\u043E\u0440\u043C\u0430 \u043A\u0430\u043B\u043E\u0440\u0438\u0439 \u0432 \u0434\u0435\u043D\u044C +userTo.name=\u0418\u043C\u044F +userTo.email=\u041F\u043E\u0447\u0442\u0430 +userTo.password=\u041F\u0430\u0440\u043E\u043B\u044C +userTo.caloriesPerDay=\u041D\u043E\u0440\u043C\u0430 \u043A\u0430\u043B\u043E\u0440\u0438\u0439 \u0432 \u0434\u0435\u043D\u044C + meal.title=\u041C\u043E\u044F \u0435\u0434\u0430 meal.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0435\u0434\u044B meal.add=\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0435\u0434\u044B @@ -33,8 +38,20 @@ common.deleted=\u0417\u0430\u043F\u0438\u0441\u044C \u0443\u0434\u0430\u043B\u04 common.saved=\u0417\u0430\u043F\u0438\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 common.enabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430 common.disabled=\u0417\u0430\u043F\u0438\u0441\u044C \u0434\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u0430 -common.errorStatus=\u0421\u0442\u0430\u0442\u0443\u0441 \u043E\u0448\u0438\u0431\u043A\u0438 -common.appError=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F +common.search=\u0418\u0441\u043A\u0430\u0442\u044C + +error.appError=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F +error.dataNotFound=\u0414\u0430\u043D\u043D\u044B\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B +error.dataError=\u041E\u0448\u0438\u0431\u043A\u0430 \u0432 \u0434\u0430\u043D\u043D\u044B\u0445 +error.validationError=\u041E\u0448\u0438\u0431\u043A\u0430 \u0444\u043E\u0440\u043C\u0430\u0442\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 exception.user.duplicateEmail=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0441 \u0442\u0430\u043A\u043E\u0439 \u043F\u043E\u0447\u0442\u043E\u0439 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0432 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 -exception.meal.duplicateDateTime=\u0423 \u0432\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0435\u0434\u0430 \u0441 \u0442\u0430\u043A\u043E\u0439 \u0434\u0430\u0442\u043E\u0439/\u0432\u0440\u0435\u043C\u0435\u043D\u0435\u043C \ No newline at end of file +exception.meal.duplicateDateTime=\u0423 \u0432\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0435\u0434\u0430 \u0441 \u0442\u0430\u043A\u043E\u0439 \u0434\u0430\u0442\u043E\u0439/\u0432\u0440\u0435\u043C\u0435\u043D\u0435\u043C + +NotEmpty=\u041F\u043E\u043B\u0435 "{0}" \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C +NotBlank=\u041F\u043E\u043B\u0435 "{0}" \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C +NotNull=\u041F\u043E\u043B\u0435 "{0}" \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C +Email=\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u043F\u043E\u043B\u044F "{0}" +Range=\u041F\u043E\u043B\u0435 "{0}" \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} +Length=\u0414\u043B\u0438\u043D\u043D\u0430 \u043F\u043E\u043B\u044F "{0}" \u0434\u043E\u043B\u0436\u0435\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} +Size=\u0420\u0430\u0437\u043C\u0435\u0440 \u043F\u043E\u043B\u044F "{0}" \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} \ 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 index 0c5e15a94910..6a1f7a16a247 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -3,11 +3,13 @@ public class ErrorInfo { private final String url; private final ErrorType type; + private final String typeMessage; private final String[] details; - public ErrorInfo(CharSequence url, ErrorType type, String... details) { + public ErrorInfo(CharSequence url, ErrorType type, String typeMessage, String... details) { this.url = url.toString(); this.type = type; + this.typeMessage = typeMessage; this.details = details; } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java index 0801d939460b..e731d70215a3 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java @@ -1,8 +1,18 @@ package ru.javawebinar.topjava.util.exception; public enum ErrorType { - APP_ERROR, - DATA_NOT_FOUND, - DATA_ERROR, - VALIDATION_ERROR + APP_ERROR("error.appError"), + DATA_NOT_FOUND("error.dataNotFound"), + DATA_ERROR("error.dataError"), + VALIDATION_ERROR("error.validationError"); + + private final String errorCode; + + ErrorType(String errorCode) { + this.errorCode = errorCode; + } + + public String getErrorCode() { + return errorCode; + } } \ 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 index 7078173057ae..4bbc90dc1c0f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -76,10 +76,7 @@ public ErrorInfo bindValidationError(HttpServletRequest req, Exception e) { ((BindException) e).getBindingResult() : ((MethodArgumentNotValidException) e).getBindingResult(); String[] details = result.getFieldErrors().stream() - .map(fe -> { - String msg = fe.getDefaultMessage(); - return (msg.startsWith(fe.getField())) ? msg : fe.getField() + ' ' + msg; - }) + .map(fe -> messageUtil.getMessage(fe)) .toArray(String[]::new); return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, details); @@ -91,7 +88,7 @@ public ErrorInfo handleError(HttpServletRequest req, Exception e) { return logAndGetErrorInfo(req, e, true, APP_ERROR); } - private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType, String... details) { + private ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType, String... details) { Throwable rootCause = ValidationUtil.getRootCause(e); if (logException) { log.error(errorType + " at request " + req.getRequestURL(), rootCause); @@ -99,6 +96,7 @@ private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); } return new ErrorInfo(req.getRequestURL(), errorType, + messageUtil.getMessage(errorType.getErrorCode()), details.length != 0 ? details : new String[]{rootCause.toString()}); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java index 0784561cd6ac..4a17313f8b58 100644 --- a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java @@ -2,11 +2,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; import ru.javawebinar.topjava.AuthorizedUser; import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ErrorType; import javax.servlet.http.HttpServletRequest; @@ -14,11 +16,15 @@ public class GlobalControllerExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalControllerExceptionHandler.class); + @Autowired + private MessageUtil messageUtil; + @ExceptionHandler(Exception.class) public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { - log.error("Exception at request " + req.getRequestURL(), e); - ModelAndView mav = new ModelAndView("exception/exception"); Throwable rootCause = ValidationUtil.getRootCause(e); + log.error("Exception at request " + req.getRequestURL(), rootCause); + ModelAndView mav = new ModelAndView("exception/exception"); + mav.addObject("typeMessage", messageUtil.getMessage(ErrorType.APP_ERROR.getErrorCode())); mav.addObject("exception", rootCause); mav.addObject("message", rootCause.toString()); diff --git a/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java index d4f2add25f95..414ca3e6f626 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java @@ -2,6 +2,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Component; @@ -24,4 +25,8 @@ public String getMessage(String code, Locale locale) { public String getMessage(String code) { return getMessage(code, LocaleContextHolder.getLocale()); } + + public String getMessage(MessageSourceResolvable resolvable) { + return messageSource.getMessage(resolvable, LocaleContextHolder.getLocale()); + } } diff --git a/src/main/webapp/WEB-INF/jsp/exception/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp index a598a65bfc73..b027313d32a0 100644 --- a/src/main/webapp/WEB-INF/jsp/exception/exception.jsp +++ b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp @@ -11,7 +11,7 @@

    -

    +

    ${typeMessage}

    ${message}

    diff --git a/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp index c669984f6340..15785fc819d6 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp @@ -4,7 +4,7 @@ \ No newline at end of file diff --git a/src/main/webapp/resources/js/datatablesUtil.js b/src/main/webapp/resources/js/datatablesUtil.js index 9d588d347bba..ff99b118dbdf 100644 --- a/src/main/webapp/resources/js/datatablesUtil.js +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -16,6 +16,25 @@ function makeEditable() { }); } +// https://api.jquery.com/jquery.extend/#jQuery-extend-deep-target-object1-objectN +function extendsOpts(opts) { + $.extend(true, opts, + { + "ajax": { + "url": ajaxUrl, + "dataSrc": "" + }, + "paging": false, + "info": true, + "language": { + "search": i18n["common.search"] + }, + "initComplete": makeEditable + } + ); + return opts; +} + function add() { $("#modalTitle").html(i18n["addTitle"]); form.find(":input").val(""); @@ -82,7 +101,7 @@ function failNoty(jqXHR) { // https://stackoverflow.com/questions/48229776 var errorInfo = JSON.parse(jqXHR.responseText); failedNote = new Noty({ - text: "  " + i18n["common.errorStatus"] + ": " + jqXHR.status + "
    " + errorInfo.type + "
    " + errorInfo.details.join("
    "), + text: "  " + errorInfo.typeMessage + "
    " + errorInfo.details.join("
    "), type: "error", layout: "bottomRight" }).show(); diff --git a/src/main/webapp/resources/js/mealDatatables.js b/src/main/webapp/resources/js/mealDatatables.js index e9ced4efa01d..266d7f781c44 100644 --- a/src/main/webapp/resources/js/mealDatatables.js +++ b/src/main/webapp/resources/js/mealDatatables.js @@ -28,13 +28,7 @@ $.ajaxSetup({ }); $(function () { - datatableApi = $("#datatable").DataTable({ - "ajax": { - "url": ajaxUrl, - "dataSrc": "" - }, - "paging": false, - "info": true, + datatableApi = $('#datatable').DataTable(extendsOpts({ "columns": [ { "data": "dateTime" @@ -64,9 +58,8 @@ $(function () { ], "createdRow": function (row, data, dataIndex) { $(row).addClass(data.exceed ? 'exceeded' : 'normal'); - }, - "initComplete": makeEditable - }); + } + })); $.datetimepicker.setLocale(localeCode); diff --git a/src/main/webapp/resources/js/userDatatables.js b/src/main/webapp/resources/js/userDatatables.js index 8581d45606dd..3f6a76537e8b 100644 --- a/src/main/webapp/resources/js/userDatatables.js +++ b/src/main/webapp/resources/js/userDatatables.js @@ -22,13 +22,7 @@ function enable(chkbox, id) { // $(document).ready(function () { $(function () { - datatableApi = $("#datatable").DataTable({ - "ajax": { - "url": ajaxUrl, - "dataSrc": "" - }, - "paging": false, - "info": true, + datatableApi = $('#datatable').DataTable(extendsOpts({ "columns": [ { "data": "name" @@ -84,7 +78,6 @@ $(function () { if (!data.enabled) { $(row).addClass("disabled"); } - }, - "initComplete": makeEditable - }); + } + })); }); \ No newline at end of file From 199ba74cc769dd5b8caf177fba763be6ea77ee0d Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:48:09 +0300 Subject: [PATCH 132/139] 11_07 XSS --- config/messages/app.properties | 3 ++- config/messages/app_ru.properties | 3 ++- pom.xml | 6 ++++++ src/main/java/ru/javawebinar/topjava/View.java | 4 ++++ .../topjava/model/AbstractNamedEntity.java | 4 ++++ .../java/ru/javawebinar/topjava/model/Meal.java | 2 ++ .../java/ru/javawebinar/topjava/model/User.java | 3 +++ .../java/ru/javawebinar/topjava/to/UserTo.java | 3 +++ .../topjava/web/meal/MealAjaxController.java | 5 +++-- .../topjava/web/meal/MealRestController.java | 7 ++++--- .../topjava/web/user/AdminRestController.java | 7 ++++--- .../topjava/web/meal/MealRestControllerTest.java | 15 +++++++++++++++ 12 files changed, 52 insertions(+), 10 deletions(-) diff --git a/config/messages/app.properties b/config/messages/app.properties index dd48a296ab40..5f8338ca970d 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -54,4 +54,5 @@ NotNull="{0}" must not be empty Email= Invalid format of "{0}" Range="{0}" must be between {2} and {1} Length="{0}" length must be between {2} and {1} -Size="{0}" size must be between {2} and {1} \ No newline at end of file +Size="{0}" size must be between {2} and {1} +SafeHtml="{0}" may has unsafe html content \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 1a136adbcd52..97d06d6db219 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -54,4 +54,5 @@ NotNull=\u041F\u043E\u043B\u0435 "{0}" \u043D\u0435 \u0434\u043E\u043B\u0436\u04 Email=\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u043F\u043E\u043B\u044F "{0}" Range=\u041F\u043E\u043B\u0435 "{0}" \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} Length=\u0414\u043B\u0438\u043D\u043D\u0430 \u043F\u043E\u043B\u044F "{0}" \u0434\u043E\u043B\u0436\u0435\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} -Size=\u0420\u0430\u0437\u043C\u0435\u0440 \u043F\u043E\u043B\u044F "{0}" \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} \ No newline at end of file +Size=\u0420\u0430\u0437\u043C\u0435\u0440 \u043F\u043E\u043B\u044F "{0}" \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {2} \u0438 {1} +SafeHtml=\u041F\u043E\u043B\u0435 "{0}" \u043C\u043E\u0436\u0435\u0442 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 html \u043A\u043E\u043D\u0442\u0435\u043D\u0442 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 336eee124d7e..d42f5ce50318 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ 5.2.12.Final 6.0.7.Final + 1.11.2 3.0.1-b08 @@ -207,6 +208,11 @@ hibernate-validator ${hibernate-validator.version} + + org.jsoup + jsoup + ${jsoup.version} + org.hibernate hibernate-jcache diff --git a/src/main/java/ru/javawebinar/topjava/View.java b/src/main/java/ru/javawebinar/topjava/View.java index 62c332bcf970..f1623c0536b0 100644 --- a/src/main/java/ru/javawebinar/topjava/View.java +++ b/src/main/java/ru/javawebinar/topjava/View.java @@ -3,5 +3,9 @@ import javax.validation.groups.Default; public class View { + // Validate only form UI/REST + public interface Web extends Default {} + + // Validate only when DB save/update public interface Persist extends Default {} } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java index be41aa890966..128a2abaebd4 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -1,5 +1,8 @@ package ru.javawebinar.topjava.model; +import org.hibernate.validator.constraints.SafeHtml; +import ru.javawebinar.topjava.View; + import javax.persistence.Column; import javax.persistence.MappedSuperclass; import javax.validation.constraints.NotBlank; @@ -12,6 +15,7 @@ public abstract class AbstractNamedEntity extends AbstractBaseEntity { @NotBlank @Size(min = 2, max = 100) @Column(name = "name", nullable = false) + @SafeHtml(groups = {View.Web.class}) protected String name; public AbstractNamedEntity() { diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 5b6fb05cde98..b0b3ab6bae55 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -3,6 +3,7 @@ import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; import org.springframework.format.annotation.DateTimeFormat; import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.util.DateTimeUtil; @@ -39,6 +40,7 @@ public class Meal extends AbstractBaseEntity { @Column(name = "description", nullable = false) @NotBlank @Size(min = 2, max = 120) + @SafeHtml(groups = {View.Web.class}) private String description; @Column(name = "calories", nullable = false) diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index f33a84bbdd28..b6c0a77c5145 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -5,7 +5,9 @@ import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; import org.springframework.util.CollectionUtils; +import ru.javawebinar.topjava.View; import javax.persistence.*; import javax.validation.constraints.Email; @@ -37,6 +39,7 @@ public class User extends AbstractNamedEntity { @Email @NotBlank @Size(max = 100) + @SafeHtml(groups = {View.Web.class}) // https://stackoverflow.com/questions/17480809 private String email; @Column(name = "password", nullable = false) diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java index 64788665c494..03994e84dd79 100644 --- a/src/main/java/ru/javawebinar/topjava/to/UserTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava.to; import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; import ru.javawebinar.topjava.util.UserUtil; import javax.validation.constraints.Email; @@ -14,11 +15,13 @@ public class UserTo extends BaseTo implements Serializable { @NotBlank @Size(min = 2, max = 100) + @SafeHtml private String name; @Email @NotBlank @Size(max = 100) + @SafeHtml // https://stackoverflow.com/questions/17480809 private String email; @Size(min = 5, max = 32, message = "length must between 5 and 32 characters") diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java index db1448fd4164..5985a693817d 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealAjaxController.java @@ -1,11 +1,12 @@ package ru.javawebinar.topjava.web.meal; import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealWithExceed; -import javax.validation.Valid; import java.time.LocalDate; import java.time.LocalTime; import java.util.List; @@ -33,7 +34,7 @@ public void delete(@PathVariable("id") int id) { } @PostMapping - public void createOrUpdate(@Valid Meal meal) { + public void createOrUpdate(@Validated(View.Web.class) Meal meal) { if (meal.isNew()) { super.create(meal); } else { diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index 396ed1a7a0f0..04aa2416b01d 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -3,12 +3,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealWithExceed; -import javax.validation.Valid; import java.net.URI; import java.time.LocalDate; import java.time.LocalTime; @@ -40,12 +41,12 @@ public List getAll() { @Override @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) - public void update(@Valid @RequestBody Meal meal, @PathVariable("id") int id) { + public void update(@Validated(View.Web.class) @RequestBody Meal meal, @PathVariable("id") int id) { super.update(meal, id); } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWithLocation(@Valid @RequestBody Meal meal) { + public ResponseEntity createWithLocation(@Validated(View.Web.class) @RequestBody Meal meal) { Meal created = super.create(meal); URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index 4a6d1247d0bb..c43bb443fe8b 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -3,11 +3,12 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.User; -import javax.validation.Valid; import java.net.URI; import java.util.List; @@ -29,7 +30,7 @@ public User get(@PathVariable("id") int id) { } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWithLocation(@Valid @RequestBody User user) { + public ResponseEntity createWithLocation(@Validated(View.Web.class) @RequestBody User user) { User created = super.create(user); // HttpHeaders httpHeaders = new HttpHeaders(); @@ -51,7 +52,7 @@ public void delete(@PathVariable("id") int id) { @Override @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) - public void update(@Valid @RequestBody User user, @PathVariable("id") int id) { + public void update(@Validated(View.Web.class) @RequestBody User user, @PathVariable("id") int id) { super.update(user, id); } diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java index 182991a2643f..81a2248701a7 100644 --- a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -13,6 +13,7 @@ import ru.javawebinar.topjava.web.AbstractControllerTest; import ru.javawebinar.topjava.web.json.JsonUtil; +import java.time.LocalDateTime; import java.util.Arrays; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -159,6 +160,20 @@ public void testUpdateInvalid() throws Exception { .andDo(print()); } + + @Test + public void testUpdateHtmlUnsafe() throws Exception { + Meal invalid = new Meal(MEAL1_ID, LocalDateTime.now(), "", 200); + mockMvc.perform(put(REST_URL + MEAL1_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(invalid)) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()) + .andExpect(errorType(ErrorType.VALIDATION_ERROR)) + .andDo(print()); + } + @Test @Transactional(propagation = Propagation.NEVER) public void testUpdateDuplicate() throws Exception { From 4de34cb3237556ce0ef5ed3c74c342e0cbfb455c Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:48:23 +0300 Subject: [PATCH 133/139] 11_08 404 --- src/main/resources/spring/spring-mvc.xml | 3 ++- src/main/webapp/WEB-INF/web.xml | 4 ++++ src/main/webapp/test.html | 13 ------------- 3 files changed, 6 insertions(+), 14 deletions(-) delete mode 100644 src/main/webapp/test.html diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index cd06762108d5..9fa5361ac93d 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -11,7 +11,8 @@ - + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index f40dd731cc78..3fa68e5ea1f6 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -30,6 +30,10 @@ contextConfigLocation classpath:spring/spring-mvc.xml + + throwExceptionIfNoHandlerFound + true + 1 diff --git a/src/main/webapp/test.html b/src/main/webapp/test.html deleted file mode 100644 index 62e151e5806f..000000000000 --- a/src/main/webapp/test.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - -
    - -
    - - \ No newline at end of file From e93f105d31463cf23ac4c11ac5afa699987ce40e Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:48:43 +0300 Subject: [PATCH 134/139] 11_09 auth user --- .../javawebinar/topjava/AuthorizedUser.java | 4 ++++ .../web/GlobalControllerExceptionHandler.java | 7 ------ .../topjava/web/RootController.java | 10 ++++---- .../web/interceptor/ModelInterceptor.java | 24 ------------------- .../web/user/ProfileRestController.java | 13 +++++----- src/main/resources/spring/spring-mvc.xml | 5 ++-- .../WEB-INF/jsp/fragments/bodyHeader.jsp | 2 +- 7 files changed, 21 insertions(+), 44 deletions(-) delete mode 100644 src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java diff --git a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java index e90610cff1a4..92386610e704 100644 --- a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java +++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java @@ -33,6 +33,10 @@ public static AuthorizedUser get() { return user; } + public int getId() { + return userTo.getId(); + } + public static int id() { return get().userTo.getId(); } diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java index 4a17313f8b58..1abe4616785f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java @@ -6,7 +6,6 @@ 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 ru.javawebinar.topjava.util.exception.ErrorType; @@ -27,12 +26,6 @@ public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) thr mav.addObject("typeMessage", messageUtil.getMessage(ErrorType.APP_ERROR.getErrorCode())); mav.addObject("exception", rootCause); mav.addObject("message", rootCause.toString()); - - // Interceptor is not invoked, put userTo - AuthorizedUser authorizedUser = AuthorizedUser.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 index aedcdaa90e4e..58916965cab1 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -2,6 +2,7 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; @@ -43,18 +44,19 @@ public String meals() { } @GetMapping("/profile") - public String profile() { + public String profile(ModelMap model, @AuthenticationPrincipal AuthorizedUser authorizedUser) { + model.addAttribute("userTo", authorizedUser.getUserTo()); return "profile"; } @PostMapping("/profile") - public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status) { + public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status, @AuthenticationPrincipal AuthorizedUser authorizedUser) { if (result.hasErrors()) { return "profile"; } try { - super.update(userTo, AuthorizedUser.id()); - AuthorizedUser.get().update(userTo); + super.update(userTo, authorizedUser.getId()); + authorizedUser.update(userTo); status.setComplete(); return "redirect:meals"; } catch (DataIntegrityViolationException ex) { diff --git a/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java deleted file mode 100644 index d667454749b7..000000000000 --- a/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java +++ /dev/null @@ -1,24 +0,0 @@ -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 javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * This interceptor adds the user to the model of every requests managed - */ -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 = AuthorizedUser.safeGet(); - if (authorizedUser != null) { - modelAndView.getModelMap().addAttribute("userTo", authorizedUser.getUserTo()); - } - } - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index ef253bae38a1..7ce2fac559f2 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -2,6 +2,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import ru.javawebinar.topjava.AuthorizedUser; import ru.javawebinar.topjava.model.User; @@ -15,19 +16,19 @@ public class ProfileRestController extends AbstractUserController { static final String REST_URL = "/rest/profile"; @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public User get() { - return super.get(AuthorizedUser.id()); + public User get(@AuthenticationPrincipal AuthorizedUser authorizedUser) { + return super.get(authorizedUser.getId()); } @DeleteMapping @ResponseStatus(value = HttpStatus.NO_CONTENT) - public void delete() { - super.delete(AuthorizedUser.id()); + public void delete(@AuthenticationPrincipal AuthorizedUser authorizedUser) { + super.delete(authorizedUser.getId()); } @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public void update(@Valid @RequestBody UserTo userTo) { - super.update(userTo, AuthorizedUser.id()); + public void update(@Valid @RequestBody UserTo userTo, @AuthenticationPrincipal AuthorizedUser authorizedUser) { + super.update(userTo, authorizedUser.getId()); } @GetMapping(value = "/text") diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml index 9fa5361ac93d..f76e9f94cedf 100644 --- a/src/main/resources/spring/spring-mvc.xml +++ b/src/main/resources/spring/spring-mvc.xml @@ -30,6 +30,9 @@
    + + + @@ -76,8 +79,6 @@ - - diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index 95acfadc4033..b2aa86121cc3 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -14,7 +14,7 @@ - ${userTo.name} + From 47bffc2ff9eaea09c48c182bc8793135a76734eb Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Wed, 24 Jan 2018 22:49:10 +0300 Subject: [PATCH 135/139] 11_10 restrict modification --- config/messages/app.properties | 2 + config/messages/app_ru.properties | 2 + .../java/ru/javawebinar/topjava/Profiles.java | 3 +- .../topjava/util/ValidationUtil.java | 4 +- .../util/exception/ApplicationException.java | 42 ++++++++++++++++++ .../ModificationRestrictionException.java | 11 +++++ .../util/exception/NotFoundException.java | 11 +++-- .../topjava/web/ExceptionInfoHandler.java | 12 ++--- .../web/GlobalControllerExceptionHandler.java | 14 +++++- .../javawebinar/topjava/web/MessageUtil.java | 13 ++++-- .../web/user/AbstractUserController.java | 21 +++++++++ src/main/resources/spring/spring-db.xml | 2 +- .../service/AbstractMealServiceTest.java | 6 ++- .../web/user/HerokuRestControllerTest.java | 44 +++++++++++++++++++ 14 files changed, 167 insertions(+), 20 deletions(-) create mode 100644 src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java create mode 100644 src/main/java/ru/javawebinar/topjava/util/exception/ModificationRestrictionException.java create mode 100644 src/test/java/ru/javawebinar/topjava/web/user/HerokuRestControllerTest.java diff --git a/config/messages/app.properties b/config/messages/app.properties index 5f8338ca970d..8636b04c6d8f 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -45,7 +45,9 @@ error.dataNotFound=Data not found error.dataError=Data error error.validationError=Validation error +exception.common.notFound=Not found entity with {0} exception.user.duplicateEmail=User with this email already exists +exception.user.modificationRestriction=Admin/User modification is forbidden exception.meal.duplicateDateTime=You already have meal with this date/time NotEmpty="{0}" must not be empty diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 97d06d6db219..56d91fd5bf1b 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -45,7 +45,9 @@ error.dataNotFound=\u0414\u0430\u043D\u043D\u044B\u0435 \u043D\u0435 \u043D\u043 error.dataError=\u041E\u0448\u0438\u0431\u043A\u0430 \u0432 \u0434\u0430\u043D\u043D\u044B\u0445 error.validationError=\u041E\u0448\u0438\u0431\u043A\u0430 \u0444\u043E\u0440\u043C\u0430\u0442\u0430 \u0434\u0430\u043D\u043D\u044B\u0445 +exception.common.notFound=\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0437\u0430\u043F\u0438\u0441\u044C \u0441 {0} exception.user.duplicateEmail=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0441 \u0442\u0430\u043A\u043E\u0439 \u043F\u043E\u0447\u0442\u043E\u0439 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0432 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 +exception.user.modificationRestriction=\u041C\u043E\u0434\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F Admin/User \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u0430 exception.meal.duplicateDateTime=\u0423 \u0432\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044C \u0435\u0434\u0430 \u0441 \u0442\u0430\u043A\u043E\u0439 \u0434\u0430\u0442\u043E\u0439/\u0432\u0440\u0435\u043C\u0435\u043D\u0435\u043C NotEmpty=\u041F\u043E\u043B\u0435 "{0}" \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java index d6ef2e0379ed..91b983429553 100644 --- a/src/main/java/ru/javawebinar/topjava/Profiles.java +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -10,7 +10,8 @@ public class Profiles { public static final String POSTGRES_DB = "postgres", - HSQL_DB = "hsqldb"; + HSQL_DB = "hsqldb", + HEROKU = "heroku"; // Get DB profile depending of DB driver in classpath public static String getActiveDbProfile() { diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 0e71260a5240..3899c2606954 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -21,9 +21,9 @@ public static T checkNotFound(T object, String msg) { return object; } - public static void checkNotFound(boolean found, String msg) { + public static void checkNotFound(boolean found, String arg) { if (!found) { - throw new NotFoundException("Not found entity with " + msg); + throw new NotFoundException(arg); } } diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java b/src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java new file mode 100644 index 000000000000..31fb5f20c576 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java @@ -0,0 +1,42 @@ +package ru.javawebinar.topjava.util.exception; + +import org.springframework.http.HttpStatus; + +import java.util.Arrays; + +public class ApplicationException extends RuntimeException { + + private final ErrorType type; + private final String msgCode; + private final HttpStatus httpStatus; + private final String[] args; + + public ApplicationException(String msgCode, HttpStatus httpStatus) { + this(ErrorType.APP_ERROR, msgCode, httpStatus); + } + + public ApplicationException(ErrorType type, String msgCode, HttpStatus httpStatus, String... args) { + super(String.format("type=%s, msgCode=%s, args=%s", type, msgCode, Arrays.toString(args))); + this.type = type; + this.msgCode = msgCode; + this.httpStatus = httpStatus; + this.args = args; + } + + public ErrorType getType() { + return type; + } + + public String getMsgCode() { + return msgCode; + } + + public HttpStatus getHttpStatus() { + return httpStatus; + } + + public String[] getArgs() { + return args; + } + +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ModificationRestrictionException.java b/src/main/java/ru/javawebinar/topjava/util/exception/ModificationRestrictionException.java new file mode 100644 index 000000000000..2f46bf152dea --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ModificationRestrictionException.java @@ -0,0 +1,11 @@ +package ru.javawebinar.topjava.util.exception; + +import org.springframework.http.HttpStatus; + +public class ModificationRestrictionException extends ApplicationException { + public static final String EXCEPTION_MODIFICATION_RESTRICTION = "exception.user.modificationRestriction"; + + public ModificationRestrictionException() { + super(EXCEPTION_MODIFICATION_RESTRICTION, HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java index f1e9b0e46376..2e41450ded44 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -1,7 +1,12 @@ package ru.javawebinar.topjava.util.exception; -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); +import org.springframework.http.HttpStatus; + +public class NotFoundException extends ApplicationException { + public static final String NOT_FOUND_EXCEPTION = "exception.common.notFound"; + + // http://stackoverflow.com/a/22358422/548473 + public NotFoundException(String arg) { + super(ErrorType.DATA_NOT_FOUND, NOT_FOUND_EXCEPTION, HttpStatus.UNPROCESSABLE_ENTITY, arg); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java index 4bbc90dc1c0f..c60c9b41a15d 100644 --- a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -7,6 +7,7 @@ import org.springframework.core.annotation.Order; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -15,9 +16,9 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestControllerAdvice; import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ApplicationException; import ru.javawebinar.topjava.util.exception.ErrorInfo; import ru.javawebinar.topjava.util.exception.ErrorType; -import ru.javawebinar.topjava.util.exception.NotFoundException; import javax.servlet.http.HttpServletRequest; import java.util.Collections; @@ -46,11 +47,10 @@ public class ExceptionInfoHandler { @Autowired private MessageUtil messageUtil; - // 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); + @ExceptionHandler(ApplicationException.class) + public ResponseEntity applicationError(HttpServletRequest req, ApplicationException appEx) { + ErrorInfo errorInfo = logAndGetErrorInfo(req, appEx, false, appEx.getType(), messageUtil.getMessage(appEx)); + return new ResponseEntity<>(errorInfo, appEx.getHttpStatus()); } @ResponseStatus(value = HttpStatus.CONFLICT) // 409 diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java index 1abe4616785f..cc6d3f59c8a8 100644 --- a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ApplicationException; import ru.javawebinar.topjava.util.exception.ErrorType; import javax.servlet.http.HttpServletRequest; @@ -18,14 +19,23 @@ public class GlobalControllerExceptionHandler { @Autowired private MessageUtil messageUtil; + @ExceptionHandler(ApplicationException.class) + public ModelAndView applicationErrorHandler(HttpServletRequest req, ApplicationException appEx) throws Exception { + return getView(req, appEx, appEx.getType(), messageUtil.getMessage(appEx)); + } + @ExceptionHandler(Exception.class) public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { + return getView(req, e, ErrorType.APP_ERROR, null); + } + + public ModelAndView getView(HttpServletRequest req, Exception e, ErrorType type, String msg) throws Exception { Throwable rootCause = ValidationUtil.getRootCause(e); log.error("Exception at request " + req.getRequestURL(), rootCause); ModelAndView mav = new ModelAndView("exception/exception"); - mav.addObject("typeMessage", messageUtil.getMessage(ErrorType.APP_ERROR.getErrorCode())); + mav.addObject("typeMessage", messageUtil.getMessage(type.getErrorCode())); mav.addObject("exception", rootCause); - mav.addObject("message", rootCause.toString()); + mav.addObject("message", msg != null ? msg : rootCause.toString()); return mav; } } diff --git a/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java index 414ca3e6f626..2891428680a7 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java @@ -5,6 +5,7 @@ import org.springframework.context.MessageSourceResolvable; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Component; +import ru.javawebinar.topjava.util.exception.ApplicationException; import java.util.Locale; @@ -18,12 +19,16 @@ public MessageUtil(MessageSource messageSource) { this.messageSource = messageSource; } - public String getMessage(String code, Locale locale) { - return messageSource.getMessage(code, null, locale); + public String getMessage(String code, Locale locale, String... args) { + return messageSource.getMessage(code, args, locale); } - public String getMessage(String code) { - return getMessage(code, LocaleContextHolder.getLocale()); + public String getMessage(String code, String... args) { + return getMessage(code, LocaleContextHolder.getLocale(), args); + } + + public String getMessage(ApplicationException appEx) { + return getMessage(appEx.getMsgCode(), appEx.getArgs()); } public String getMessage(MessageSourceResolvable resolvable) { diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index f6bd95465fe2..090dc5a920c6 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -3,9 +3,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import ru.javawebinar.topjava.Profiles; +import ru.javawebinar.topjava.model.AbstractBaseEntity; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.exception.ModificationRestrictionException; import java.util.List; @@ -18,6 +22,13 @@ public abstract class AbstractUserController { @Autowired private UserService service; + private boolean modificationRestriction; + + @Autowired + public void setEnvironment(Environment environment) { + modificationRestriction = environment.acceptsProfiles(Profiles.HEROKU); + } + public List getAll() { log.info("getAll"); return service.getAll(); @@ -36,18 +47,21 @@ public User create(User user) { public void delete(int id) { log.info("delete {}", id); + checkModificationAllowed(id); service.delete(id); } public void update(User user, int id) { log.info("update {} with id={}", user, id); assureIdConsistent(user, id); + checkModificationAllowed(id); service.update(user); } public void update(UserTo userTo, int id) { log.info("update {} with id={}", userTo, id); assureIdConsistent(userTo, id); + checkModificationAllowed(id); service.update(userTo); } @@ -58,6 +72,13 @@ public User getByMail(String email) { public void enable(int id, boolean enabled) { log.info((enabled ? "enable " : "disable ") + id); + checkModificationAllowed(id); service.enable(id, enabled); } + + private void checkModificationAllowed(int id) { + if (modificationRestriction && id < AbstractBaseEntity.START_SEQ + 2) { + throw new ModificationRestrictionException(); + } + } } \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 0d0ee5071d6a..1946ca8f8f47 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -34,7 +34,7 @@ p:password="${database.password}"/> - + Date: Wed, 24 Jan 2018 22:50:03 +0300 Subject: [PATCH 136/139] 11_11 heroku --- Procfile | 1 + hr.bat | 2 + pom.xml | 42 +++++++++++++++++++ settings.xml | 10 +++++ src/main/resources/db/heroku.properties | 5 +++ src/main/resources/spring/spring-db.xml | 26 +++++++++++- .../web/user/HerokuRestControllerTest.java | 23 ++++++++++ system.properties | 1 + 8 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 Procfile create mode 100644 hr.bat create mode 100644 settings.xml create mode 100644 src/main/resources/db/heroku.properties create mode 100644 system.properties diff --git a/Procfile b/Procfile new file mode 100644 index 000000000000..4afeb7900918 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java $JAVA_OPTS -Dspring.profiles.active="datajpa,heroku" -DTOPJAVA_ROOT="." -jar target/dependency/webapp-runner.jar --port $PORT target/*.war \ No newline at end of file diff --git a/hr.bat b/hr.bat new file mode 100644 index 000000000000..1ea30584df86 --- /dev/null +++ b/hr.bat @@ -0,0 +1,2 @@ +call mvn -B -s settings.xml -DskipTests=true clean package +call java -Dspring.profiles.active="datajpa,heroku" -DDATABASE_URL="postgres://user:password@localhost:5432/topjava" -jar target/dependency/webapp-runner.jar target/*.war \ No newline at end of file diff --git a/pom.xml b/pom.xml index d42f5ce50318..021290947ac3 100644 --- a/pom.xml +++ b/pom.xml @@ -401,6 +401,48 @@ true + + heroku + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.0.2 + + + package + + copy + + + + + com.github.jsimone + webapp-runner + 8.5.11.2 + webapp-runner.jar + + + + + + + + + + + org.postgresql + postgresql + ${postgresql.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat.version} + + + diff --git a/settings.xml b/settings.xml new file mode 100644 index 000000000000..e0a6d49fb0d0 --- /dev/null +++ b/settings.xml @@ -0,0 +1,10 @@ + + + + + + heroku + + diff --git a/src/main/resources/db/heroku.properties b/src/main/resources/db/heroku.properties new file mode 100644 index 000000000000..c8146ba6f139 --- /dev/null +++ b/src/main/resources/db/heroku.properties @@ -0,0 +1,5 @@ +jpa.showSql=false +hibernate.format_sql=false +hibernate.use_sql_comments=false +database.init=false +jdbc.initLocation=initDB.sql \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 1946ca8f8f47..303ffeb0fb7c 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -34,7 +34,7 @@ p:password="${database.password}"/> - + + + + + + + + + + Date: Wed, 24 Jan 2018 22:50:20 +0300 Subject: [PATCH 137/139] 11_12 fix locale --- src/main/java/ru/javawebinar/topjava/web/MessageUtil.java | 1 + .../java/ru/javawebinar/topjava/web/AbstractControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java index 2891428680a7..0ed3701537bb 100644 --- a/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/MessageUtil.java @@ -11,6 +11,7 @@ @Component public class MessageUtil { + public static final Locale RU_LOCALE = new Locale("ru"); private final MessageSource messageSource; diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java index ad027ef6f6a1..44bc7e4a46f0 100644 --- a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -77,7 +77,7 @@ public void setUp() { } protected String getMessage(String code) { - return messageUtil.getMessage(code); + return messageUtil.getMessage(code, MessageUtil.RU_LOCALE); } public ResultMatcher errorType(ErrorType type) { From b75ff8936325d646b02d11d8c0bf18179995279e Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sun, 4 Feb 2018 19:46:42 +0300 Subject: [PATCH 138/139] doc fix --- doc/cv.md | 868 +++++++++++++++++++++++++++++++++++++ doc/graduation.md | 184 ++++---- doc/lesson04.md | 99 +++-- doc/lesson08.md | 84 ++-- doc/lesson10.md | 971 ++++++++++++++++++++++++++++++++++++++++++ doc/lesson11.md | 1042 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 3088 insertions(+), 160 deletions(-) create mode 100644 doc/cv.md create mode 100644 doc/lesson10.md create mode 100644 doc/lesson11.md diff --git a/doc/cv.md b/doc/cv.md new file mode 100644 index 000000000000..5ba6e7bc9295 --- /dev/null +++ b/doc/cv.md @@ -0,0 +1,868 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + topjava/cv.md at master · JavaOPs/topjava + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Skip to content +
    + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +
    +
    + + + + + + + +
    +
    + +
      +
    • +
      + +
      + + + +
      +
      +
      + + Notifications +
      + + + +
      +
      +
      +
      +
    • + +
    • + +
      +
      + + + +
      +
      + + + +
      + +
    • + +
    • +
      + +
      + +
    • +
    + +

    + + /topjava + +

    + +
    + + + + +
    + +
    +
    + + + Permalink + + + +
    + +
    + + +
    + +
    +
    + + Switch branches/tags +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    +
    + + +
    + +
    Nothing to show
    +
    + +
    +
    +
    + +
    + + Find file + + +
    + +
    + + + +
    + + + 52f9ec6 + + Dec 20, 2017 + +
    + @JavaOPs + + Add info +
    + +
    + + +
    + + +
    + + +
    +
    +
    + +
    + Raw + Blame + History +
    + + + + + +
    + +
    +
    + +
    + +
    + 106 lines (85 sloc) + + 11.4 KB +
    +
    + + +
    +

    Составление резюме, подготовка к интервью, поиск работы

    +

    cv

    + +

    Составление резюме:

    + +

    Наши истории (делимся опытом и успехом)

    +

    Тесты/задачи онлайн:

    + +

    Тестовое собеседование, самые спрашиваемые темы

    +

    Интервью:

    + +
    +

    про clone и finalize объязательно прочтите Джошуа Блох: Java. Эффективное программирование (второе издание)

    +
    + +
    +

    Особенно обратите внимание на раздел: Часть 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 (в 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 события, интересные видео и многое другое.

      +
    • +
    • +

      Подпишитесь на рассылку вакансий под себя

      +
    • +
    +

    После прохождения испытательного срока жду твою историю успеха

    +

    Основные сайты поиска работы:

    + +

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

    +
      +
    • Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В +процессе формулирования очень часто приходит ее решение.
    • +
    • Учись инвестигировать проблему. Внимательное чтение логов и умение дебажить - основные навыки +разработчика. В логах надо читать верх самого нижнего эксепшена - там причина всей портянки.
    • +
    • Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и +бится нам ней часами. +Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время.
    • +
    • Если тебе что-то объясняют по проекту - обязательно записывай.
    • +
    • Когда получаешь задачу - уточни все очень подробно.
    • +
    • Получай в процессе решения обратную связь - в том ли направлении ты идешь.
    • +
    • Не игнорируй совместные ланчи (курилки)
    • +
    • Готовься к стендапам/летучкам. Задавай на них вменяемые вопросы. Выказывай заинтересованность
    • +
    • Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи.
    • +
    • Советы новичкам
    • +
    +

    Отзывы по стажировке Topjava

    +
    +
    + +
    + + + + + +
    + +
    + +
    +
    + +
    + + + + + + +
    + + + You can't perform that action at this time. +
    + + + + + + + + + + +
    + + You signed in with another tab or window. Reload to refresh your session. + You signed out in another tab or window. Reload to refresh your session. +
    + + + + + + diff --git a/doc/graduation.md b/doc/graduation.md index c98758c76e64..6030266e6436 100644 --- a/doc/graduation.md +++ b/doc/graduation.md @@ -18,8 +18,8 @@ - - + + @@ -36,19 +36,21 @@ - + - + - - + + + - + + @@ -63,13 +65,13 @@ - + - + - + @@ -178,7 +180,7 @@
    -
    @@ -285,21 +286,22 @@ -
    -
    -
    +
    +
    +
    -
    -
    -
      + @@ -593,15 +605,15 @@
      - - 5b5c8c6 + + 9ec0cfc - Nov 23, 2017 + Jan 21, 2018
      @@ -641,12 +653,13 @@ -
      - -
      +
      + +
      +
      - 68 lines (52 sloc) + 72 lines (55 sloc) - 8.13 KB + 9.02 KB
      -

      Graduation project

      -

      Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас конечно есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и очевидно (если конечно в ТЗ нет оговорок). Все свои вопросы и предложения и хотелки оформляйете отдельно (в read.me например). Если делаете что-то сложнее простейшего случая (например справочник еды)- объязательно напишите в read.me. Как и выбор стратегии кэширования.

      -
      -

      Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять

      -
      -

      Антуан де Сент-Экзюпери

      -
      +

      Тестовое задание на оплачиваемую стажировку

      Design and implement a REST API using Hibernate/Spring/SpringMVC (or Spring-Boot) without frontend.

      The task is:

      Build a voting system for deciding where to have lunch.

      @@ -686,19 +693,27 @@

    Each restaurant provides new menu each day.

    -

    As a result, provide a link to github repository. It should contain the code, README.md with API documentation and couple curl commands to test it.

    +

    As a result, provide a link to github repository.

    +

    It should contain the code and README.md with API documentation and curl commands to get data for voting and vote.


    -

    10 Best Practices for Better RESTful API

    P.S.: Make sure everything works with latest version that is on github :)

    P.P.S.: Asume that your API will be used by a frontend developer to build frontend on top of that.


    error Рекомендации

    -

    Сделайте новый проект и добавляйте туда из Topjava только то что нужно! Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - не нужны!

    -

    API продумывайте с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться

    +
      +
    • Сделай новый проект и добавляй туда из Topjava только то что нужно! Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - не нужны!
    • +
    • API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (frontend)
    • +
    • Сначала сделай основной сценарий по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом.
    • +
    +

    Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас конечно есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если конечно в ТЗ нет оговорок). Все свои вопросы и предложения и хотелки оформляйете отдельно (в read.me например). Если делаете что-то сложнее простейшего случая (например справочник еды)- объязательно напишите в read.me. Как и выбор стратегии кэширования.

    +
    +

    Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять

    +
    +

    Антуан де Сент-Экзюпери

    • 1: читаем ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять
    • 2: тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу
    • -
    • 3: тщательно считайте количество запросов в вашем API для отображения нужной информации
    • +
    • 3: тщательно считайте количество запросов в вашем API для отображения нужной информации и чтобы проголосовать
    • 4: учитывайте, что пользователей может быть ооочень много, а админов- мало
    • 5: в проекте (и тестовом задании на работу) в отличие от нашего учебного topjava оставляйте только необходимый для работы приложения код, ничего лишнего:
        @@ -712,20 +727,26 @@
      • 6: базу лучше взять без установки (H2 или HSQLDB)
      • 7: по возможности сделать JUnit тесты
      • 8: уделяйте внимание обработке ошибок
      • -
      • 9: далаем REST API в соответствии с концепцией REST, с учетом иерархии принадлежности объектов
      • +
      • 9: далаем REST API в соответствии с концепцией REST, с учетом иерархии принадлежности объектов + +
      • 10: не смешивайте TO и Entity вместе. Лучше всего, если они будут независимыми друг от друга.
      • 11: если приложению в объекте требуется только его id, используйте reference (как мы при сохранении еды вставляем туда юзера)
      • 12: Use for money in java app
      • 13: Историю еды и голосований лучше сделать. Нужно различать базовые вещи, которые закладываются в архитектуру приложения и неочевидные доработки к ТЗ, которых лучше не делать.
      • -
      • 14: Еще раз про hashCode/equals в Entity: не делайте сравнение по всем полям!
      • +
      • 14: Еще раз про hashCode/equals в Entity: не делайте сравнение по полям!!
      • 15: Название пакетов, имен классов для model/to/web достаточно стандартные (например model/domain). НЕ надо придумывать своих собственных правил.
      • 16: Предпочтительно использовать DATA-JPA (можно без лишней делегации, напрямую из сервиса/контроллера дергать Repository). В случае JPA позаботьтесь о своем собственном generic DAO.
      • -
      • 17: Вместо ModelMatcher (который нуждается в javadoc) в тестах контроллеров можно использовать json-path и xxx.json("{...}"), патч 11_04_HW10_duplicate_datetime
      • -
      • 18: На topjava мы смотрели разные варианты использования, тут делаем максимально просто. С TO многие вещи упрощаются.
      • -
      • 19: Проверьте, не торчат ли из кода учебные уши topjava, типа ProfileRestController.testUTF(), AbstractServiceTest.printResult() или закомментированные JdbcTemplate. Назначение этого проекта совсем другое.
      • -
      • 20: ORM работает с объектами. В простейших случаях fk_id как поля допустимы, но при этом JPA их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverwrflow в коде обязательна!
      • -
      • 21: Проверьте, станет ли код проще с @AuthenticationPrincipal (урок 11, Доступ к AuthorizedUser). С ним можно убрать из AuthorizedUser все статические методы.
      • +
      • 17: На topjava мы смотрели разные варианты использования, тут делаем максимально просто. С TO многие вещи упрощаются.
      • +
      • 18: Проверьте, не торчат ли из кода учебные уши topjava, типа ProfileRestController.testUTF(), AbstractServiceTest.printResult() или закомментированные JdbcTemplate. Назначение этого проекта совсем другое.
      • +
      • 19: ORM работает с объектами. В простейших случаях fk_id как поля допустимы, но при этом JPA их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverwrflow в коде обязательна!
      • +
      • 20: Проверьте, станет ли код проще с @AuthenticationPrincipal (урок 11, Доступ к AuthorizedUser). С ним можно убрать из AuthorizedUser все статические методы.
      • +
      • 21: Не размещайте логику приложения и преобразования в TO в слое доступа к DB
      +

      Отнеситесь к проекту как к вашему тестовому заданию на работу! Это означает, что НЕ будет возможности объяснять, почему ты так сделал код и что для запуска надо создать базу данных или переменную окружения. Если что-то не по ТЗ или не запускается и нет инструкции или код непонятный и нет комментариев - молча вычеркнут из кандидатов.

    @@ -738,6 +759,7 @@
    +
    @@ -751,7 +773,7 @@ +
    +
    + + + + +
    + +
    + +
    +
    + + + +
    +
    +
    + + + + + + + +
    +
    + +
      +
    • +
      + +
      + + + +
      +
      +
      + + Notifications +
      + + + +
      +
      +
      +
      +
    • + +
    • + +
      +
      + + + +
      +
      + + + +
      + +
    • + +
    • +
      + +
      + +
    • +
    + +

    + + /topjava + +

    + +
    + + + + +
    + +
    +
    + + + Permalink + + + +
    + +
    + + +
    + +
    +
    + + Switch branches/tags +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    +
    + + +
    + +
    Nothing to show
    +
    + +
    +
    +
    + +
    + + Find file + + +
    + +
    + + + +
    + Fetching contributors… +
    + +
    + + Cannot retrieve contributors at this time +
    +
    + +
    +
    +
    + +
    + Raw + Blame + History +
    + + + + + +
    + +
    +
    + +
    + +
    + 188 lines (132 sloc) + + 19.8 KB +
    +
    + + +
    +

    Онлайн проекта Topjava

    +

    Материалы занятия

    +

    hw Разбор домашнего задания HW9

    +

    video 1. HW9

    +

    Apply 10_1_HW9_binding_ajax.patch

    +

    Datatables перевели на ajax ("ajax": {"url": ajaxUrl, ..), те при отрисовке таблица сама по этому url запрашивает данные. Поэтому в методе RootController.meals() нам нужно только возвратить view "meals" (meals.jsp) которому уже не нужны данные в атрибутах.

    +
    +
      +
    • JavaScript i18n[] локализацию перенес в i18n.jsp
    • +
    • Вынес общий код в ValidationUtil.getErrorResponse()
    • +
    +
    +

    Apply 10_2_HW9_test.patch

    +

    Apply 10_3_HW9_datetimepicker.patch

    +
    +
      +
    • Изменил формат ввода dateTime в форме без 'T': при биндинге значений к полям формы в datatablesUtil.updateRow() для поля dateTime делаю replace('T', ' '). REST интерфейс по прежнему работает в стандарте ISO-8601
    • +
    • Вынес общий код в datatablesUtil.formatDate()
    • +
    +
    + +

    Занятие 10:

    +

    video 2. Дополнительно: кастомизация JSON (@JsonView) и валидации (groups)

    +

    ВНИМАНИЕ! 10_04_opt и 10_05_opt - не обязательные патчи. Если будете делать- то в отдельной ветке (у меня opt_view_groups). Это варианты решений, которые не идут в master.

    + +

    Apply 10_04_opt_json_view.patch

    + +

    Apply 10_05_opt_validated_groups.patch

    + +

    video 3. Рефакторинг: jQuery конверторы и группы валидации по умолчанию

    +

    Apply 10_04_jquery_converters.patch

    + +

    Apply 10_05_persist_validate_group.patch

    +
      +
    • Default Hibernate validation. Т.к. Persist наследуется от javax.validation.groups.Default, при сохранении учитываются все непомеченные аннотации валидации (Default) + помеченные Persist.
    • +
    +

    video 4. Spring Security Taglib. Method Security Expressions.

    +

    Apply 10_06_secure_tag_annotation.patch

    + +

    question Вопрос:

    +
    +

    У нас в sprng-mvc подключается spring-app, в который подключается spring-security. Т.о. spring-mvc в себе содержит код spring-app и spring-security. Почему тогда аннотация <security:global-method-security... из spring-security не видна для контроллера из spring-mvc?

    +
    +
      +
    1. Подключаем один контекст внутрь другого: это про import а не про наследование (тоже самое что скопипастить сюда xml из подключаемого import файла). Наследование означает, что поиск бина, если он не найден, происходит в родителе.
    2. +
    3. При создании контекста аннотация <security:global-method-security... говорит про то, что все бины текущего контекста, у которых есть аннотации авторизации (@PreAuthorize/ @Secured/ ..) будут проксироваться с проверкой авторизации. Контексты родителей и детей создаются сами по себе и аннотации проксирования у них собственные.
    4. +
    +

    video 5. Interceptors. Редактирование профиля. JSP tag files.

    +

    Apply 10_07_interceptor.patch

    + +

    Apply 10_08_profile_jsptag.patch

    + +

    video 6. Форма регистрации.

    +

    Apply 10_09_registration.patch

    +
    +
      +
    • Добавил локализацию
    • +
    • При регистрации передаю username в login.jsp как параметр и делаю setCredentials (пароль вводится скрыто, поэтому его не передаю из соображений безопасности). Т.к setCredentials требует jQuery, убрал для него defer
    • +
    +
    +

    video 7. Обработка исключений в Spring.

    +

    Apply 10_10_not_found_422.patch

    +
    +

    Поменял код 404 (URL not found) на 422 (Unprocessable Entity)

    +
    + +

    Apply 10_11_global_exception.patch

    +
    +
      +
    • Перед отображением exception предварительно делаю ValidationUtil.getRootCause
    • +
    • Добавил локализацию
    • +
    +
    + +

    Apply 10_12_controller_advice_exception.patch

    +

    Отображение валидации для формы еды и юзера пропало (TODO в коде для HW10). Можно посмотреть сериализацию в ErrorInfo при попытке добавить юзера с дублирующимся email.

    +
    +
      +
    • Добавил в ErrorInfo тип ошибки ErrorType
    • +
    • @ResponseBody над методами ExceptionInfoHandler заменил на @RestControllerAdvice
    • +
    • В ExceptionInfoHandler удалил @Order над методами и добавил над классом:
    • +
    +
    +
      Methods are matched by closest exception in
    +  *  @see  org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#getMappedMethod
    +  *  164: Collections.sort(matches, new ExceptionDepthComparator(exceptionType))
    +
    +
    +
      +
    • В ExceptionInfoHandler.logAndGetErrorInfo() также использую ValidationUtil.getRootCause
    • +
    +
    + +

    video 8. Encoding password. Json READ/WRITE access

    +

    Apply 10_13_password_encoding.patch

    + +

    Apply 10_14_read_write_access.patch

    +

    В реальном приложении для управления паролем необходим отдельный UI интерфейс с подтверждением старого пароля.

    + +

    video 9. Межсайтовая подделка запроса (CSRF).

    +

    Apply 10_15_csrf.patch

    +
    +

    Убрал form:form из ajax запросов: там csrf работает через header. Проверьте в вкладке браузера Network.

    +
    +

    Поломалась UTF-8 кодировка в редактировании профиля и регистрациию (если по умолчанию не UTF-8). В Optional HW10 нужно будет починить.

    + +

    question Ваши вопросы

    +
    +

    В чем отличие между аннотоацией @PreAuthorize("hasRole('ROLE_ADMIN')") и конфигурацией в jsp: <sec:authorize access="isAuthenticated()">, <sec:authorize access="hasRole('ROLE_ADMIN')"> ?

    +
    +

    Анотация @PreAuthorize обрабатывается Spring анологично @Transactional, @Cacheable - класс проксируется и до-после вызова метода добавляется функциональность. +В данном случае перед вызовом метода проверяются роль залогиненного юзера. JSTL тэг authorize выполняет проверку условия в залогиненном юзере внутри jsp.

    +
    +

    Еще раз: почему не нужен csrf для REST и нельзя подделать JSON запрос с вредоносного сайта?

    +
    +

    Попробуйте выполнить ajax запрос из вашего приложения c url, у которого домен отличный от вашего (например "http://topjava.herokuapp.com/meals/ajax/admin/users/"+id). +В консоли браузера будет XMLHttpRequest cannot load... - нарушение same origin policy. +Формам же разрешается делать submit (через action=..) на другой домен, но невозможно cделать Content-Type, отличный от стндартных enctype и методов кроме get и post. Таким образом consumes = MediaType.APPLICATION_JSON_VALUE в POST защищает приложение от CSRF.

    +
    +

    Почему использован BCryptPasswordEncoderа не hash(password+salt). Почему нельзя напрямую сравнивать закодированные пароли?

    +
    +

    BCryptPasswordEncoder automatically generates a salt and concatenates it with the hash value in a single String. Для одного и тогоже пароля получаются разные хэши. Cравнивать их можно только через isMatch() с незахэшированным паролем.

    +
    +

    Когда запускается в GlobalControllerExceptionHandler метод defaultErrorHandler? Когда как в него исключение попадает? Как выбирается, кто обрабатывает исключения: ExceptionInfoHandler или GlobalControllerExceptionHandler?

    +
    +

    GlobalControllerExceptionHandler попадает в контекст спринг (через @ControllerAdvice его находят в пакете web). Далее спринг перехватывает исключения и отправляет в подходящий по исключению метод GlobalControllerExceptionHandler. ExceptionInfoHandler помечен @ControllerAdvice(annotations = RestController.class), он обрабатывает только ошибки из всех контроллеров с аннотацией RestController.class.

    +
    +

    Откуда берутся в валидации сообщения на русском "должно быть между 10 и 10000"?

    +
    +

    Локализация встроена в Hibernate Validation. Смотрите Ctrl+Shift+N и ValidationMessages_ru.properties.

    +

    hw Домашнее задание HW10

    +
      +
    • 1: Сделать валидацию в AdminAjaxController/MealAjaxController через ExceptionInfoHandler. Вернуть клиенту ErrorInfo и статус HttpStatus.UNPROCESSABLE_ENTITY (тип методов контроллеров вернуть обратно на void).
    • +
    • 2: Сделать валидацию принимаемых json объектов в REST контроллерах через ExceptionInfoHandler. Добавить в Rest контроллеры тест для невалидных данных. + +
    • +
    • 3: Сделать обработку ошибки при дублирования email ("User with this email already exists") для: +
        +
      • 3.1 регистрации / редактирования профиля пользователя + +
      • +
      • 3.2 добавления / редактирования пользователя в таблице
      • +
      • 3.3 REST контроллеров
      • +
      +
    • +
    +

    Optional

    + +
    +

    error Проверка в HW10

    +
      +
    • 1: ErrorInfo просто бин для передачи информации на клиента. Кода возврата и ответ настраиваются в ExceptionInfoHandler.
    • +
    • 2: Не дублируйте обработку ошибок BindingResult: result.getFieldErrors()..
    • +
    • 3: Можно не создавать собственные эксепшены, а в ExceptionInfoHandler ловить стандартные
    • +
    • 4: в MethodArgumentNotValidException также есть e.getBindingResult(), его можно обрабатывать по аналогии с BindException
    • +
    • 5: Не дублируйте код переключения локали на странице логина и в приложении
    • +
    • 6: При проблемах с валидацией Meals в MealRestController, посмотрите на валидацию в MealAjaxController.updateOrCreate
    • +
    +
    +
    + +
    + + + + + +
    + +
    + +
    +
    + +
    + + + + + + +
    + + + You can't perform that action at this time. +
    + + + + + + + + + + +
    + + You signed in with another tab or window. Reload to refresh your session. + You signed out in another tab or window. Reload to refresh your session. +
    + + + + + + diff --git a/doc/lesson11.md b/doc/lesson11.md new file mode 100644 index 000000000000..ba2e0092f619 --- /dev/null +++ b/doc/lesson11.md @@ -0,0 +1,1042 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + topjava/lesson11.md at doc · JavaWebinar/topjava + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Skip to content +
    + + + + + + + + + + + + +
    + +
    + +
    +
    + + + +
    +
    +
    + + + + + + + +
    +
    + +
      +
    • +
      + +
      + + + +
      +
      +
      + + Notifications +
      + + + +
      +
      +
      +
      +
    • + +
    • + +
      +
      + + + +
      +
      + + + +
      + +
    • + +
    • +
      + +
      + +
    • +
    + +

    + + /topjava + +

    + +
    + + + + +
    + +
    +
    + + + Permalink + + + +
    + +
    + + +
    + +
    +
    + + Switch branches/tags +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    +
    + + +
    + +
    Nothing to show
    +
    + +
    +
    +
    + +
    + + Find file + + +
    + +
    + + + +
    + + + 104349b + + Jan 26, 2018 + + + +
    + + +
    + + +
    + + +
    +
    +
    + +
    + Raw + Blame + History +
    + + + + + +
    + +
    +
    + +
    + +
    + 204 lines (168 sloc) + + 21.3 KB +
    +
    + + +
    +

    Онлайн проекта Topjava

    +

    Обновил Демо приложение нашей версией

    +

    Материалы занятия

    +

    Некоторые темы урока новые, поэтому видео есть не везде.

    +

    Все материалы проекта (в том числе и будущие обновления) останутся доступны в Google Drive.

    +

    error Правка

    +

    Apply 11_0_fix.patch

    + +

    hw Разбор домашнего задания HW10

    +

    video 1. HW10

    +

    Apply 11_01_HW10_fix_encoding.patch

    +

    Apply 11_02_HW10_validation.patch

    +
    + +
    +

    Apply 11_03_HW10_duplicate_email.patch

    +
    +
      +
    • сделал код(ключ) i18n константой (EXCEPTION_DUPLICATE_EMAIL)
    • +
    +
    +

    Apply 11_04_HW10_duplicate_datetime.patch

    +
    +
      +
    • Реализавать обработку дублирования user.email и meal.dateTime можно по разному + +
    • +
    +
    +
    + +
    +

    question Вопрос:

    +
    +

    Поясните, почему без тесты на дубликаты падают без @Transactional(propagation = Propagation.NEVER)

    +
    +
      +
    • @Transactional в AbstractControllerTest означает, что коммита в базу не происходит, вместо него делается rollback. +А без сохранения в базу нарушение ограничения на дубликат не происходит.
    • +
    • @Transactional(propagation = Propagation.NEVER) над тестом отключает AbstractControllerTest @Transactional, в репозитории происходит сохранение в базу и в ExceptionInfoHandler ловим DataIntegrityViolationException.
    • +
    +

    video 2. HW10 Optional: change locale

    +

    Apply 11_05_HW10_change_locale.patch

    +
    +
      +
    • Вместо lang.jsp сделал общий bodyHeader.jsp
    • +
    • Добавил локализацию календаря $.datetimepicker.setLocale(localeCode)
    • +
    • Вместо смену локали в lang.jsp через javascript сделал href=${requestScope['javax.servlet.forward.request_uri']}?lang=..
    • +
    +
    +

    Bootstrap Navbar

    +

    Заключительное 11-е занятие

    +

    Локализация:

    +

    Apply 11_06_i18n.patch

    +
      +
    • Вынес общие части опций таблиц в datatablesUtil.js используя jQuery.extend()
    • +
    • Добавил локализацию Search в datatable
    • +
    • Сделал локализацию ошибок валидации: +
        +
      • Все коды для локализации конкретной ошибки валидации можно посмотреть в дебаге: FieldError.getCodes()
      • +
      • Коды для ее аргументов: FieldError.getArguments() (аргумент складывается из имени класса и поля).
      • +
      +
    • +
    • Добавил локализацию ErrorInfo.type (код локализации в ErrorType и поле ErrorInfo.typeMessage)
    • +
    +

    Защита от XSS (Cross Site Scripting)

    +
    +

    Попробуйте до и после патча ввести в любое текстовое поле редактирования <script>alert('XSS')</script> и сохранить.

    +
    +

    Apply 11_07_XSS.patch

    +
    +
      +
    • password проверять не надо, т.к. он не выводится в html, а email надо
    • +
    • Сделал общий интерфейс валидации View.Web и @Validated(View.Web.class) вместо @Valid для проверки содержимого только на входе UI/REST. При сохранении проверка на безопасный html контент не делается.
    • +
    +
    + +

    Обработка ошибок 404 (NotFound)

    +

    Apply 11_08_404.patch

    +

    Доступ к AuthorizedUser

    +

    Apply 11_09_auth_user.patch

    +
      +
    • Автоподстановка в контроллерах +
        +
      • не стал делать автоподстановку по всем контроллерам (в абстрактных контроллерах проще работать с AuthorizedUser, чем получать его через @AuthenticationPrincipal и передавать параметром)
      • +
      +
    • +
    • В JSP: the authentication Tag +
        +
      • авторизованный пользователь доступен в JSP через tag authentication, интерсептор становится не нужным
      • +
      +
    • +
    +

    Ограничение модификации пользователей

    +

    Apply 11_10_restrict_modification.patch

    +
      +
    • В AbstractUserController добавилась защита от удаления-модификации Admin/User для профиля HEROKUAbstractUserController заинжектил Environment и сделал проверку HEROKU)
    • +
    • Чтобы тесты были рабочими, ввел профиль HEROKU, работающий так же, как и POSTGRES.
    • +
    • Добавил ApplicationException для работы с ошибками с поддержкой i18n в приложении (от него отнаследовал NotFoundException и ModificationRestrictionException)
    • +
    +

    video 3. Деплой приложения в Heroku.

    +

    Apply 11_11_heroku.patch

    +
    + +
    +

    Приложение деплоится в ROOT: http://localhost:8080

    + +
    Config Vars
    +  ERROR_PAGE_URL=...
    +  TOPJAVA_ROOT=/app
    +
    +Datasources advanced
    +    ssl=true
    +    sslfactory=org.postgresql.ssl.NonValidatingFactory
    +
    + +

    Apply 11_12_fix_locale.patch

    +
      +
    • Починка тестов, зависящих от локали окружения
    • +
    +

    video 4. Собеседование. Разработка ПО

    + +

    hw Домашнее задание по проекту:

    +
      +
    • Задеплоить свое приложение в Heroku
    • +
    • Сделать / обновить резюме (отдать на ревью в канал #hw11 группы slack) + +
    • +
    • Заполнить в своем профиле Java Online Projects ссылку на резюме и информацию по поиску работы (если конечно актуально): резюме, флаги рассматриваю работу, готов к релокации и информация для HR.
    • +
    • Обновления базы соискателей буду рассылать по HR ночью на 31.01, постарайтесь успеть
    • +
    • Сделать и внести в свое портфолио Graduation project +
        +
      • Для участников с проверкой выпускной проект принимаю на ревью до 15.02 (участникам Masterjava до 31.05).
      • +
      +
    • +
    • Пройти важную тему МНОГОПОТОЧНОСТЬ по первому уроку Masterjava. +
        +
      • Обучение на Masterjava сейчас идет в индивидуальном режиме, старт в любое время.
      • +
      • Проект, патчи, группа Slack, занятия и видео анологичны проекту Topjava.
      • +
      • Цена для участников Topjava снижена
      • +
      +
    • +
    +

    Возможные доработки приложения:

    +
      +
    • Для редактирования паролей сделать отдельный интерфейс с запросом старого пароля и кнопку сброса пароля для администратора.
    • +
    • Добавление и удаление ролей для пользователей в админке.
    • +
    • Перевести UI на Angular / Vaadin elements /GWT /GXT /Vaadin / ZK/ [Ваш любимый фреймворк]..
    • +
    • Перевести проект на Spring Boot
    • +
    • Перевести шаблоны с JSP на Thymeleaf
    • +
    • Сделать авторизацию в приложение по OAuth 2.0 (Spring Security OAuth, +VK auth, github oauth, ...)
    • +
    • Сделать подтверждение регистрации пользователя по email c возможностью восстановления пароля.
    • +
    • Сделать отображение еды постранично, с поиском и сортировкой на стороне сервера.
    • +
    • Перевод проекта на https
    • +
    • Сделать desktop/mobile приложение, работающее по REST с нашим приложением.
    • +
    • Показ ошибок в модальном окне редактирования таблицы так же, как и в JSP профиля
    • +
    • Limit login attempts example
    • +
    • Сделать авторизацию REST по JWT
    • +
    +

    Доработки участников прошлых выпусков:

    + +

    Жду твою доработку из списка!

    +

    Ресурсы по Проекту

    + +

    Пожалуйста ответь на 4-6 вопросов по проекту, это важно!

    +

    error Замечания по резюме:

    +
      +
    • варианты размещения: google doc + linkedin + hh + ...
    • +
    • хорошо, если будет в html формате (например в https://pages.github.com/). Например так, на github
    • +
    • возраст и день рождения можно не писать
    • +
    • все упоминания Junior убрать!
    • +
    • линки делай кликабельными (если формат поддерживает)
    • +
    • если нет опыта в IT, обязательно вставь участие в стажировке Topjava. Также вставь ссылку на свой сертификат (она есть в личном профиле). Весь не-IT опыт можно кратко.
    • +
    • всю выгодную для себя информацию (и важную для HR) распологайте вверху. Название секций в резюме и их порядок относительно стандартный и важный
    • +
    +
    +
    + +
    + + + + + +
    + +
    + +
    +
    + +
    + + + + + + +
    + + + You can't perform that action at this time. +
    + + + + + + + + + + +
    + + You signed in with another tab or window. Reload to refresh your session. + You signed out in another tab or window. Reload to refresh your session. +
    + + + + + + From e3f21ee36c5977cba59a6cb79a31908846ded6ee Mon Sep 17 00:00:00 2001 From: Rimskiy Date: Sat, 10 Feb 2018 17:32:56 +0300 Subject: [PATCH 139/139] doc fix md --- doc/cv.md | 973 ++++---------------------------- doc/graduation.md | 907 +++--------------------------- doc/lesson01.md | 1315 +++++++++---------------------------------- doc/lesson02.md | 1185 +++++++-------------------------------- doc/lesson03.md | 1249 ++++++++--------------------------------- doc/lesson04.md | 1227 +++++++--------------------------------- doc/lesson05.md | 1202 +++++++-------------------------------- doc/lesson06.md | 1327 +++++++++----------------------------------- doc/lesson07.md | 1036 +++++----------------------------- doc/lesson08.md | 1181 ++++++--------------------------------- doc/lesson09.md | 1101 +++++------------------------------- doc/lesson10.md | 1100 ++++++------------------------------ doc/lesson11.md | 1233 +++++++--------------------------------- doc/meals_index.md | 905 +++--------------------------- 14 files changed, 2403 insertions(+), 13538 deletions(-) diff --git a/doc/cv.md b/doc/cv.md index 5ba6e7bc9295..7a0900afddc6 100644 --- a/doc/cv.md +++ b/doc/cv.md @@ -1,868 +1,105 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - topjava/cv.md at master · JavaOPs/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - 52f9ec6 - - Dec 20, 2017 - -
    - @JavaOPs - - Add info -
    - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    -
    - -
    - -
    - 106 lines (85 sloc) - - 11.4 KB -
    -
    - - -
    -

    Составление резюме, подготовка к интервью, поиск работы

    -

    cv

    - -

    Составление резюме:

    - -

    Наши истории (делимся опытом и успехом)

    -

    Тесты/задачи онлайн:

    - -

    Тестовое собеседование, самые спрашиваемые темы

    -

    Интервью:

    - -
    -

    про clone и finalize объязательно прочтите Джошуа Блох: Java. Эффективное программирование (второе издание)

    -
    - -
    -

    Особенно обратите внимание на раздел: Часть 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 (в 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 события, интересные видео и многое другое.

      -
    • -
    • -

      Подпишитесь на рассылку вакансий под себя

      -
    • -
    -

    После прохождения испытательного срока жду твою историю успеха

    -

    Основные сайты поиска работы:

    - -

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

    -
      -
    • Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В -процессе формулирования очень часто приходит ее решение.
    • -
    • Учись инвестигировать проблему. Внимательное чтение логов и умение дебажить - основные навыки -разработчика. В логах надо читать верх самого нижнего эксепшена - там причина всей портянки.
    • -
    • Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и -бится нам ней часами. -Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время.
    • -
    • Если тебе что-то объясняют по проекту - обязательно записывай.
    • -
    • Когда получаешь задачу - уточни все очень подробно.
    • -
    • Получай в процессе решения обратную связь - в том ли направлении ты идешь.
    • -
    • Не игнорируй совместные ланчи (курилки)
    • -
    • Готовься к стендапам/летучкам. Задавай на них вменяемые вопросы. Выказывай заинтересованность
    • -
    • Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи.
    • -
    • Советы новичкам
    • -
    -

    Отзывы по стажировке Topjava

    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +## Составление резюме, подготовка к интервью, поиск работы + +![cv](https://cloud.githubusercontent.com/assets/13649199/10877471/93ea86b8-8157-11e5-9bfa-95e3fba75c58.jpg) + +- Научиться программировать сложнее, чем кажется +- [Собеседование. Разработка ПО. Вопросы.](https://drive.google.com/open?id=0B9Ye2auQ_NsFQVc2WUdCR0xvLWM) +- [Набор ссылок для тренировки и прохождения интервью](https://github.com/andreis/interview) + +### Составление резюме: +- [VisualCV: create resume in minutes](https://www.visualcv.com/) +- Выбрать шаблон для резюме +- [GitHub Pages](https://pages.github.com/), Resume template +- Как продать свое резюме в 2 раза дороже +- Как правильно составить резюме +- Резюме программистов. Часть 1 (плохие) +- Резюме программистов. Часть 2 (хорошие) +- Как составить резюме на английском +- ОФОРМЛЕНИЕ IT-РЕЗЮМЕ для USA + +### Наши истории (делимся опытом и успехом) + +### Тесты/задачи онлайн: +- [Java Programming Test](https://tests4geeks.com/java) +- game: test Java skills +- Codility lesson tests +- Quizful- тесты онлайн +- LeetCode Online Judge +- Sphere online judge +- Codility programmers lessons +- Hackerrank practice coding + +## [Тестовое собеседование, самые спрашиваемые темы](http://javaops.ru/interview/test.html) + +### Интервью: +- Михаил Портнов. Собеседование на работу: как продать себя грамотно +- Михаил Портнов. Какие вопросы мы задаем на собеседовании? +- Михаил Портнов. Собеседование на работу: жизненный путь +- Канал: Резюме, поиск работы, интервью +- Яков Файн: Как стать профессиональным Java разработчиком +- Ответы на вопросы на собеседовании Junior Java Developer +- Список вопросов с ответами для собеседования по Java +- Сборка по вопросам на интервью +- Сборка вопросов-ответов от JavaStudy +- Тест на знание 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 (в linkedin: https://www.linkedin.com/company/java-online-projects). +- После завершения проекта ты освошь все заявленные в нем технологии - вставь их в квалификацию (включая java 8 Stream and Time API). +- В разделе опыт работы (если нет коммерческого опыта) вставь: + + Участие в разработке Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей + на стеке Maven/ Spring MVC/ Security/ REST(Jackson)/ Java 8 Stream API: + - реализация сохранения в базы Postgres и HSQLDB на основе Spring JBDC, JPA(Hibernate) и Spring-Data-JPA + - реализация и тестирование REST и AJAX контроллеров + - реализация клиента на Bootstrap (css/js), datatables, jQuery + plugins. + - собственная доработка проекта + +- Делай упор не на обучение, а на **участие в проекте**. Выполнение домашних заданий это полноценное участие с написанием функционала по всем пройденным технологиям. На собеседовании смотрят не на то, что ты заканчивал, а на опыт и знания. + +### В процессе обучения +- Если рассмотриваешь предложения по работе, подними в своем профиле этот флаг и обязательно заполни ссылку на резюме. Обновления нашей базы выпускников смотрят уже более 125 партнеров по трудоустройству (компании и индивидуальные рекрутеры). Проверь содержание "Информация для HR": по нему принимают решение, открывать резюме или нет. + +- Вступайте в нашу группу участников Slack: каналы помощи с Java, отзывы о работодателях, обсуждение тестовых заданий, вакансии, цены на рынке труда, IT события, интересные видео и многое другое. + +- Подпишитесь на рассылку вакансий под себя + +### После прохождения испытательного срока жду твою [историю успеха](http://javaops.ru/view/story) + +### Основные сайты поиска работы: +- Яндекс агрегатор +- HH +- LinkedIn +- djinni.co (более актуально для Украины) + +## Как выжить на испытательном сроке +- Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В + процессе формулирования очень часто приходит ее решение. +- Учись инвестигировать проблему. Внимательное чтение логов и умение дебажить - основные навыки + разработчика. В логах надо читать верх самого нижнего эксепшена - там причина всей портянки. +- Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и + бится нам ней часами. + Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время. +- Если тебе что-то объясняют по проекту - обязательно записывай. +- Когда получаешь задачу - уточни все очень подробно. +- Получай в процессе решения обратную связь - в том ли направлении ты идешь. +- Не игнорируй совместные ланчи (курилки) +- Готовься к стендапам/летучкам. Задавай на них вменяемые вопросы. Выказывай заинтересованность +- Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи. +- [**Советы новичкам**](http://blog.csssr.ru/2016/09/19/how-to-be-a-beginner-developer) + +## [Отзывы по стажировке Topjava](https://vk.com/topic-74381644_30447246) \ No newline at end of file diff --git a/doc/graduation.md b/doc/graduation.md index 6030266e6436..f29dab87b2f0 100644 --- a/doc/graduation.md +++ b/doc/graduation.md @@ -1,836 +1,71 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - topjava/graduation.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - 9ec0cfc - - Jan 21, 2018 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    -
    - -
    - -
    - 72 lines (55 sloc) - - 9.02 KB -
    -
    - - -
    -

    Тестовое задание на оплачиваемую стажировку

    -

    Design and implement a REST API using Hibernate/Spring/SpringMVC (or Spring-Boot) without frontend.

    -

    The task is:

    -

    Build a voting system for deciding where to have lunch.

    -
      -
    • 2 types of users: admin and regular users
    • -
    • Admin can input a restaurant and it's lunch menu of the day (2-5 items usually, just a dish name and price)
    • -
    • Menu changes each day (admins do the updates)
    • -
    • Users can vote on which restaurant they want to have lunch at
    • -
    • Only one vote counted per user
    • -
    • If user votes again the same day: -
        -
      • If it is before 11:00 we asume that he changed his mind.
      • -
      • If it is after 11:00 then it is too late, vote can't be changed
      • -
      -
    • -
    -

    Each restaurant provides new menu each day.

    -

    As a result, provide a link to github repository.

    -

    It should contain the code and README.md with API documentation and curl commands to get data for voting and vote.

    -
    -

    P.S.: Make sure everything works with latest version that is on github :)

    -

    P.P.S.: Asume that your API will be used by a frontend developer to build frontend on top of that.

    -
    -

    error Рекомендации

    -
      -
    • Сделай новый проект и добавляй туда из Topjava только то что нужно! Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - не нужны!
    • -
    • API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (frontend)
    • -
    • Сначала сделай основной сценарий по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом.
    • -
    -

    Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас конечно есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если конечно в ТЗ нет оговорок). Все свои вопросы и предложения и хотелки оформляйете отдельно (в read.me например). Если делаете что-то сложнее простейшего случая (например справочник еды)- объязательно напишите в read.me. Как и выбор стратегии кэширования.

    -
    -

    Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять

    -
    -

    Антуан де Сент-Экзюпери

    -
      -
    • 1: читаем ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять
    • -
    • 2: тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу
    • -
    • 3: тщательно считайте количество запросов в вашем API для отображения нужной информации и чтобы проголосовать
    • -
    • 4: учитывайте, что пользователей может быть ооочень много, а админов- мало
    • -
    • 5: в проекте (и тестовом задании на работу) в отличие от нашего учебного topjava оставляйте только необходимый для работы приложения код, ничего лишнего: -
        -
      • 5.1 НЕ надо делать разные профили базы и работы с ней.
      • -
      • 5.2 НЕ надо делать абстрактных контроллеров на всякий случай.
      • -
      • 5.3 НЕ надо делать классов репозиториев, если там нет ничего, кроме делегирования.
      • -
      • 5.4 Из потребностей приложения (которую надо самим придумать) реализовывать только очевидные сценарии. Те.- НИЧЕГО ЛИШНЕГО.
      • -
      • 5.5 НЕ надо все бездумно кэшировать
      • -
      -
    • -
    • 6: базу лучше взять без установки (H2 или HSQLDB)
    • -
    • 7: по возможности сделать JUnit тесты
    • -
    • 8: уделяйте внимание обработке ошибок
    • -
    • 9: далаем REST API в соответствии с концепцией REST, с учетом иерархии принадлежности объектов - -
    • -
    • 10: не смешивайте TO и Entity вместе. Лучше всего, если они будут независимыми друг от друга.
    • -
    • 11: если приложению в объекте требуется только его id, используйте reference (как мы при сохранении еды вставляем туда юзера)
    • -
    • 12: Use for money in java app
    • -
    • 13: Историю еды и голосований лучше сделать. Нужно различать базовые вещи, которые закладываются в архитектуру приложения и неочевидные доработки к ТЗ, которых лучше не делать.
    • -
    • 14: Еще раз про hashCode/equals в Entity: не делайте сравнение по полям!!
    • -
    • 15: Название пакетов, имен классов для model/to/web достаточно стандартные (например model/domain). НЕ надо придумывать своих собственных правил.
    • -
    • 16: Предпочтительно использовать DATA-JPA (можно без лишней делегации, напрямую из сервиса/контроллера дергать Repository). В случае JPA позаботьтесь о своем собственном generic DAO.
    • -
    • 17: На topjava мы смотрели разные варианты использования, тут делаем максимально просто. С TO многие вещи упрощаются.
    • -
    • 18: Проверьте, не торчат ли из кода учебные уши topjava, типа ProfileRestController.testUTF(), AbstractServiceTest.printResult() или закомментированные JdbcTemplate. Назначение этого проекта совсем другое.
    • -
    • 19: ORM работает с объектами. В простейших случаях fk_id как поля допустимы, но при этом JPA их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverwrflow в коде обязательна!
    • -
    • 20: Проверьте, станет ли код проще с @AuthenticationPrincipal (урок 11, Доступ к AuthorizedUser). С ним можно убрать из AuthorizedUser все статические методы.
    • -
    • 21: Не размещайте логику приложения и преобразования в TO в слое доступа к DB
    • -
    -

    Отнеситесь к проекту как к вашему тестовому заданию на работу! Это означает, что НЕ будет возможности объяснять, почему ты так сделал код и что для запуска надо создать базу данных или переменную окружения. Если что-то не по ТЗ или не запускается и нет инструкции или код непонятный и нет комментариев - молча вычеркнут из кандидатов.

    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +## Тестовое задание на оплачиваемую стажировку + +Design and implement a REST API using Hibernate/Spring/SpringMVC (or Spring-Boot) **without frontend**. + +The task is: + +Build a voting system for deciding where to have lunch. + + * 2 types of users: admin and regular users + * Admin can input a restaurant and it's lunch menu of the day (2-5 items usually, just a dish name and price) + * Menu changes each day (admins do the updates) + * Users can vote on which restaurant they want to have lunch at + * Only one vote counted per user + * If user votes again the same day: + - If it is before 11:00 we asume that he changed his mind. + - If it is after 11:00 then it is too late, vote can't be changed + +Each restaurant provides new menu each day. + +As a result, provide a link to github repository. + +It should contain the code and **README.md with API documentation and curl commands to get data for voting and vote.** + +----------------------------- +P.S.: Make sure everything works with latest version that is on github :) + +P.P.S.: Asume that your API will be used by a frontend developer to build frontend on top of that. + +----------------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Рекомендации + +- **Сделай новый проект и добавляй туда из Topjava только то что нужно! Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - не нужны!** +- **API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (frontend)** +- **Сначала сделай основной сценарий по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом.** + +*Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас конечно есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если конечно в ТЗ нет оговорок). Все свои вопросы и предложения и хотелки оформляйете отдельно (в `read.me` например). Если делаете что-то сложнее простейшего случая (например справочник еды)- объязательно напишите в read.me. Как и выбор стратегии кэширования.* + +> Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять + +_Антуан де Сент-Экзюпери_ + +- 1: **читаем ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять** +- 2: **тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу** +- 3: **тщательно считайте количество запросов в вашем API для отображения нужной информации и чтобы проголосовать** +- 4: **учитывайте, что пользователей может быть ооочень много, а админов- мало** +- 5: в проекте (и тестовом задании на работу) в отличие от нашего учебного topjava оставляйте только необходимый для работы приложения код, ничего лишнего: + - 5.1 НЕ надо делать разные профили базы и работы с ней. + - 5.2 НЕ надо делать абстрактных контроллеров на всякий случай. + - 5.3 НЕ надо делать классов репозиториев, если там нет ничего, кроме делегирования. + - 5.4 Из потребностей приложения (которую надо самим придумать) реализовывать только очевидные сценарии. Те.- НИЧЕГО ЛИШНЕГО. + - 5.5 НЕ надо все бездумно кэшировать +- 6: базу лучше взять без установки (H2 или HSQLDB) +- 7: по возможности сделать JUnit тесты +- 8: уделяйте внимание обработке ошибок +- 9: далаем REST API в соответствии с концепцией REST, **с учетом иерархии принадлежности объектов** + - [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) + - 10 Best Practices for Better RESTful API +- 10: не смешивайте TO и Entity вместе. Лучше всего, если они будут независимыми друг от друга. +- 11: если приложению в объекте требуется только его id, используйте reference (как мы при сохранении еды вставляем туда юзера) +- 12: [Use for money in java app](http://stackoverflow.com/a/43051227/548473) +- 13: **Историю еды и голосований лучше сделать. Нужно различать базовые вещи, которые закладываются в архитектуру приложения и неочевидные доработки к ТЗ, которых лучше не делать.** +- 14: Еще раз про [hashCode/equals в Entity](https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma): не делайте сравнение по полям!! +- 15: Название пакетов, имен классов для `model/to/web` достаточно стандартные (например `model/domain`). НЕ надо придумывать своих собственных правил. +- 16: Предпочтительно использовать DATA-JPA (можно без лишней делегации, напрямую из сервиса/контроллера дергать Repository). В случае JPA позаботьтесь о своем собственном generic DAO. +- 17: На topjava мы смотрели разные варианты использования, тут делаем максимально просто. С TO многие вещи упрощаются. +- 18: Проверьте, не торчат ли из кода учебные уши topjava, типа `ProfileRestController.testUTF()`, `AbstractServiceTest.printResult()` или закомментированные `JdbcTemplate`. Назначение этого проекта совсем другое. +- 19: ORM работает с объектами. [В простейших случаях fk_id как поля допустимы](https://stackoverflow.com/questions/6311776/hibernate-foreign-keys-instead-of-entities), но при этом JPA их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverwrflow в коде обязательна! +- 20: Проверьте, станет ли код проще с `@AuthenticationPrincipal` (урок 11, Доступ к AuthorizedUser). С ним можно убрать из `AuthorizedUser` все статические методы. +- 21: Не размещайте логику приложения и преобразования в TO в слое доступа к DB + +#### Отнеситесь к проекту как к вашему тестовому заданию на работу! Это означает, что НЕ будет возможности объяснять, почему ты так сделал код и что для запуска надо создать базу данных или переменную окружения. Если что-то не по ТЗ или не запускается и нет инструкции или код непонятный и нет комментариев - молча вычеркнут из кандидатов. \ No newline at end of file diff --git a/doc/lesson01.md b/doc/lesson01.md index 12579644dcd2..b7ed63a24096 100644 --- a/doc/lesson01.md +++ b/doc/lesson01.md @@ -1,1081 +1,280 @@ +# Онлайн проект Topjava +- **Не стоит стремиться прочитать все ссылки урока, их можно использовать как справочник. Гораздо важнее пройти основной материал урока и сделать Домашнее Задание** +- **Обязательно посмотри правила работы с патчами на проекте** +- **Делать Apply Patch лучше по одному, непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (`Version Control->Local Changes-> Ctrl+D`)** +- **Код проекта обновляется и не всегда совпадает с видео (можно увидеть как развивался проект). Изменения в проекте указываю после соответствующего патча.** +## Материалы занятия (скачать все патчи можно через `Download/Скачать` папки patch) +![image](https://cloud.githubusercontent.com/assets/13649199/18330295/5f2ca214-7560-11e6-8e1e-c0494f798c37.png) +### ![correction](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Рефакторинг проекта +#### Apply 1_0_fix.patch +> - переименовал классы `UserMeal*` в более красивые `Meal*` +> - удалил себя из шапки классов +> - обновил версию `maven-compiler-plugin` + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW0: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Optional: реализация getFilteredMealsWithExceeded через Stream API +#### Apply 1_1_HW0_stream.patch + +- [Презентация Java 8](https://docs.google.com/presentation/d/1oltLkHK60FqIdsXjUdm4pPLSeC6KpNYjDsM0ips-qBw) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Работа с git в IDEA. Реализация через цикл. +#### Apply 1_2_HW0_cycle.patch + +> Изменения в проекте: `map.getOrDefault` заменил на `map.merge`, `for` заменены на `forEach` + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW0 + +> что делает метод `Map.merge` ? + +никогда не надо лениться зайти в код `Map.merge` (`Ctrl+мышка` на методе) и почитать javadoc (`Ctrl+Q`). Когда привыкать, если не сейчас? + +> что означает `Integer::sum` ? + +это ссылка на метод, сокращенная форма лямбды. IDEA иногда выделяет желтым и предлагает замену (соглашаться по `Alt+Enter`). +Например `m->m.getCalories()` заменяется на `Meal::getCalories`. + +> почему не использовать в `TimeUtil` методы `isBefore/isAfter` ? + +это строгие (excluded) сравнения, а нам также нужны краевые значения + +> В `MealsUtil` у нас где-то есть ключевое слово `final`, где-то нет. В чем разница? + +Я участвовал в одном проекте, где `final` был обязательным (в сеттингах IDEA галочка стояла). Но это скорее исключение, чем правило в проектах java (в Java 8 вообще ввели эффективный final, те по факту). Во всех новомодных языках переменные final по умолчанию, а в java нужно помнить и везде добавлять, утомительно. Но если приучитесь - хуже не будет. Я обычно ставлю там, где важно по смыслу (если не забываю). +> Почему у `User.registered` тип `Date`, а `Meal.dateTime` `LocalDateTime `? +Время регистрации - абсолютное (конкретный момент), а время еды по бизнес логике относительно (те не зависит от часового пояса) - - - - - - - - - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. HW0 Optional 2: реализация в один проход и одной строкой Stream API +#### Apply 1_3_HW0_optional2.patch +## Занятие 1: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Обзор используемых в проекте технологий. Интеграция ПО. +- [**RebelLabs Developer Productivity Report 2017: Why do you use the Java tools you use**](https://zeroturnaround.com/rebellabs/developer-productivity-report-2017-why-do-you-use-java-tools-you-use/) +- Обзор популярности инструментов и технологий Java за 2016 г. +- Обзор популярности инструментов и технологий Java за 2014 г. +- Видео "Приложение Spring Pet Clinic" +- Приложение Spring Pet Clinic. - - - - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Maven. +- Среда сборки проектов Maven. +- The Central Repository +- Настройка пропертей Maven: кодировка, java version, зависимости, maven-compiler-plugin +- The Reactor. Snapshots +- Недостатки Maven. Другие инструменты сборки. +- Справочник: + - Видео: Maven vs Gradle vs SBT (Архипов, Борисов, Садогурский) + - Автоматизация сборки проекта + - Home Page + - Maven: The Complete Reference + - Разработка ПО вместе с maven + - Build Lifecycle + - Dependency Mechanism + - Создание своих архетипов и каталогов в Maven + - Зависимости, профили + - Bintray: gateway to Maven Central - - - topjava/lesson01.md at doc · JavaWebinar/topjava - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. WAR. Веб-контейнер Tomcat. Сервлеты. +> - **Нам нужна 8-я версия Tomcat (например [последняя 8.5.16](http://apache-mirror.rbc.ru/pub/apache/tomcat/tomcat-8/v8.5.16/bin/))** +> - **Устанавливать Tomcat лучше простым скачиванием архива `apache-tomcat-8.5.16[-windows-xxx].zip` и копированием из него в каталог без пробелов и русских букв (пример `С:\java\apache-tomcat-8.5.16`)**. Не используйте Tomcat 8.5.13 и 9.0.0.M19 IDEA [из-за бага Tomcat](https://stackoverflow.com/questions/43354676) - - +#### Apply 1_4_switch_to_war.patch +> - Обновил сервлеты до версии 3.1 +(Tomcat 8 использует это API, хотя для нас не принципиально, т.к. мы никакие фичи 3.x Servlet API не используем) +> - Переименовал `userList.jsp` в `users.jsp` +> - Сервлет добавляется в следующем патче, те **в `web.xml` он будет подчеркиваться красным.** + +#### Apply 1_5_add_servlet_api.patch + +> - **Если зависимость `servlet-api` не подтянулась, обновите maven проект.** +> - [Проверка, кто занял порт](https://stackoverflow.com/a/38953356/548473) (в случае проблем с запуском и дебагом на портах 8080, 8000) +> - [**Деплой war в Tomcat с Application context**](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%94%D0%B5%D0%BF%D0%BB%D0%BE%D0%B9-war-%D0%B2-tomcat-application-context-%D0%B4%D0%BE%D0%BB%D0%B6%D0%B5%D0%BD-%D0%B1%D1%8B%D1%82%D1%8C-%D1%82%D0%BE%D1%82-%D0%B6%D0%B5-%D1%87%D1%82%D0%BE-%D0%B8-url-%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F-%D0%B4%D0%B5%D0%BF%D0%BB%D0%BE%D0%B8%D1%82%D1%8C-%D0%BD%D0%B0%D0%B4%D0%BE-war-exploded) +> - [**Динамическое обновление без передеплоя**](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%94%D0%BB%D1%8F-%D0%B4%D0%B8%D0%BD%D0%B0%D0%BC%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9-%D0%BF%D0%B5%D1%80%D0%B5%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B8-%D1%80%D0%B5%D1%81%D1%83%D1%80%D1%81%D0%BE%D0%B2-%D0%BA%D0%BD%D0%BE%D0%BF%D0%BA%D0%B0-%D0%BD%D0%B0%D0%B6%D0%BC%D0%B8%D1%82%D0%B5-%D0%BA%D0%BD%D0%BE%D0%BF%D0%BA%D1%83-update-resource-on-frame-deactivation) + +#### Apply 1_6_forward_to_redirect.patch + +- Tomcat Home Page +- Сервлеты. +- Томкат менеджер: [http://localhost:8080/manager](http://localhost:8080/manager) + - в `TOMCAT_HOME\conf\tomcat-users.xml` нужно добавить +``` + +``` +- Наше приложение: [http://localhost:8080/topjava](http://localhost:8080/topjava) +- Наш сервлет: [http://localhost:8080/topjava/users](http://localhost:8080/topjava/users) +- Справочник: + - [HTTP](https://developer.mozilla.org/ru/docs/Web/HTTP) + - Отладчик IntelliJ IDEA + - Yakov Fain: Intro to Java EE. Glassfish. Servlets (по-русски) + - Yakov Fain: HTTP Sessions, Cookies, WAR deployments, JSP (по-русски) + - Golovach Courses: Junior.February2014.Servlets + - Remotely debug on tomcat from IDEA + - Java Server Page + - Java объекты, доступные в JSP + - Java Server Pages (JSP) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Логирование. +#### Apply 1_7_logging.patch + +> **Установите переменную окружения на TOPJAVA_ROOT на корень проекта и перезапустите IDEA. Слеши в пути должны быть в стиле unix (/)** + +> - изменения в проекте: убрал `LoggerWrapper` и логирую напрямую в логгер SLF4J. При логгировании через вспомогательный класс, в логе теряется имя исходного класса. +> - удалил зависимость `jul-to-slf4j`. Она нам не нужна и, согласно видео Владимира Красильщика про логирование, она замедляет работу +> - Не делать конкатенацию строк при логгировании сообщений, если уровень логирования в конфигурации выставлен выше уровня логирования в коде +> - [slf4j formatting with {}](http://stackoverflow.com/a/10596390/548473) +> - [What is the fastest way of (not) logging](https://www.slf4j.org/faq.html#logging_performance) + +- Java Logging: история кошмара +- Ведение лога приложения +- Log4j, Logback +- Добавление зависимостей логирования в проект. +- Logback variable substitution + +#### Проверочные вопросы: +- Что нужно изменить в `pom.xml`, чтобы перейти с logback на log4j ? + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> Почему `private static final Logger log` а не `LOG` ? + +- Так ее называют по умолчанию в проекте lombook. +- Это [правило именования констант, которые не "deeply immutable"](https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names), те если их содержимое можно изменить. + +> Используются ли еще где-то в реальной разработке JSP, или это уже устаревшая технология? Заменит ли ее JSF (https://javatalks.ru/topics/38037)? + +JSF и JSP- разные ниши и задачи. +JSP- шаблонизатор, JSF- МVС фреймворк. Из моего опыта- с JSP сталкивался в 60% проектов. Его прямая замена: http://www.thymeleaf.org (в Spring-Boot по умолчанию), но в уже запущенных проектах встречается достаточно редко. JSP не умирает, потому что просто и дешево. Кроме того включается в большинство веб-контейнеров (в Tomcat его реализация Jasper) + +JSF- sun-овский еще фреймворк, с которым я ни разу не сталкивался и особого желания нет. Вот он как раз, по моему мнению, активно замещается хотя бы javascript фреймворками (angular, ember, react). + +> А зачем мы использовали logback? Почему SLF4J нас не устроило? Почему реализация логирования не log4j? + +`SLF4J-API` это API. Там есть только пустая реализация `org.slf4j.helpers.NOPLogger` (можно посмотреть в исходниках). Logback для новых проектов стал стандарт. *spring-petclinic* и *spring-boot* используют его по умолчанию. +- http://logback.qos.ch/reasonsToSwitch.html + +--------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW01 (делаем ветку HW01 от последнего патча в master) + +> На проекте мы **учимся мыслить и работать как Java разработчики уже сейчас**, потом это будет гораздо сложнее и стоить дороже. +Вот на мой взгляд [хорошие советы новичкам](http://blog.csssr.ru/2016/09/19/how-to-be-a-beginner-developer). От себя я добавлю: +> - Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В процессе формулирования очень часто приходит ее решение. +> - Учись исследовать проблему. Внимательное чтение логов и [умение дебажить](http://info.javarush.ru/idea_help/2014/01/22/Руководство-пользователя-IntelliJ-IDEA-Отладчик-.html) - основные навыки разработчика. Обычно самый верх самого нижнего эксепшена- причина ошибки, туда нужно ставить брекпойнт. +> - Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и биться над ней часами. Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время. + +#### 1. Реализовать сервлет с отображением в таблице списка еды (в памяти и БЕЗ учета пользователя) + +> Деплоиться в Tomcat лучше как `war exploded`: нет упаковки в war и при нажатой кнопке `Update Resources on Frame Deactivation` можно обновляться css, html, jsp без передеплоя. При изменении `web.xml`, добавлении методов, классов необходим redeploy. + +- 1.1 По аналогии с `UserServlet` добавить `MealServlet` и `meals.jsp` + - Задеплоить приложение (war) в Tomcat c `applicationContext=topjava` (приложение должно быть доступно по http://localhost:8080/topjava) + - Попробовать разные деплои в Tomcat, remote и local debug +- 1.2 Сделать отображения списка еды в JSP, цвет записи в таблице зависит от параметра `exceed` (красный/зеленый). + - 1.2.1 Список еды захардкодить (те проинициализировать в коде, желательно чтобы в проекте инициализация была только в одном месте) + - 1.2.2 Время выводить без 'T' + - 1.2.3 Список выводим БЕЗ фильтрации по `startTime/endTime` + - 1.2.4 Вариант реализации: + - из сервлета преобразуете еду в `List`; + - кладете список в запрос (`request.setAttribute`); + - делаете `forward` на jsp для отрисовки таблицы (при `redirect` атрибуты теряются). + - в `JSP` для цикла можно использовать `JSTL tag forEach`. Для подключения `JSTL` в `pom.xml` и шапку JSP нужно добавить: +``` + + javax.servlet + jstl + 1.2 + + + <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + ... +``` -
    - Skip to content -
    + - Интернет-приложения на JAVA + - JSP + - JSTL для написания JSP страниц + - JSTL: Шаблоны для разработки веб-приложений в java + - JSTL LocalDateTime format - - - +### Optional +#### 2. Реализуем в ПАМЯТИ CRUD (create/read/update/delete) для еды +**Пример: Simple CRUD using Servlet/JSP** +- 2.1 Хранение в памяти будет одна из наших CRUD реализаций (позже будет JDBC, JPA и DATA-JPA). +- 2.2 Работать с реализацией CRUD через интерфейс, который не должен ничего знать о деталях реализации (Map, DB или что-то еще). +- 2.3 Добавить поле `id` в `Meal/ MealWithExceed` и реализовать генерацию счетчика, УЧЕСТЬ МНОГОПОТОЧНОСТЬ СЕРВЛЕТОВ + - [обзор java.util.concurrent](https://habrahabr.ru/company/luxoft/blog/157273/) +- 2.4 Сделать форму редактирования в JSP: AJAX/JavaScript использовать НЕ надо, делаем через `
    ` и `doPost()` в сервлете. +- 2.5 Для ввода дат и времени можно использовать html5 типы, хотя они поддерживаются не всеми браузерами (протестировать свой браузер). В конце курса мы добавим DateTimePicker jQuery plugin, который будет работать на всех браузерах. +## После выполнения ДЗ обязательно проверьте решение на ошибки +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW1 - -
    - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - 01d16ad - - Nov 9, 2017 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    - -
    - -
    - 281 lines (211 sloc) - - 33.1 KB -
    -
    - - -
    -

    Онлайн проект Topjava

    -
      -
    • Не стоит стремиться прочитать все ссылки урока, их можно использовать как справочник. Гораздо важнее пройти основной материал урока и сделать Домашнее Задание
    • -
    • Обязательно посмотри правила работы с патчами на проекте
    • -
    • Делать Apply Patch лучше по одному, непосредственно перед видео на эту тему, а при просмотре видео сразу отслеживать все изменения кода проекта по изменению в патче (Version Control->Local Changes-> Ctrl+D)
    • -
    • Код проекта обновляется и не всегда совпадает с видео (можно увидеть как развивался проект). Изменения в проекте указываю после соответствующего патча.
    • -
    -

    Материалы занятия (скачать все патчи можно через Download/Скачать папки patch)

    -

    image

    -

    correction Рефакторинг проекта

    -

    Apply 1_0_fix.patch

    -
    -
      -
    • переименовал классы UserMeal* в более красивые Meal*
    • -
    • удалил себя из шапки классов
    • -
    • обновил версию maven-compiler-plugin
    • -
    -
    -

    hw Разбор домашнего задания HW0:

    -

    video 1. Optional: реализация getFilteredMealsWithExceeded через Stream API

    -

    Apply 1_1_HW0_stream.patch

    - -

    video 2. Работа с git в IDEA. Реализация через цикл.

    -

    Apply 1_2_HW0_cycle.patch

    -
    -

    Изменения в проекте: map.getOrDefault заменил на map.merge, for заменены на forEach

    -
    -

    question Вопросы по HW0

    -
    -

    что делает метод Map.merge ?

    -
    -

    никогда не надо лениться зайти в код Map.merge (Ctrl+мышка на методе) и почитать javadoc (Ctrl+Q). Когда привыкать, если не сейчас?

    -
    -

    что означает Integer::sum ?

    -
    -

    это ссылка на метод, сокращенная форма лямбды. IDEA иногда выделяет желтым и предлагает замену (соглашаться по Alt+Enter). -Например m->m.getCalories() заменяется на Meal::getCalories.

    -
    -

    почему не использовать в TimeUtil методы isBefore/isAfter ?

    -
    -

    это строгие (excluded) сравнения, а нам также нужны краевые значения

    -
    -

    В MealsUtil у нас где-то есть ключевое слово final, где-то нет. В чем разница?

    -
    -

    Я участвовал в одном проекте, где final был обязательным (в сеттингах IDEA галочка стояла). Но это скорее исключение, чем правило в проектах java (в Java 8 вообще ввели эффективный final, те по факту). Во всех новомодных языках переменные final по умолчанию, а в java нужно помнить и везде добавлять, утомительно. Но если приучитесь - хуже не будет. Я обычно ставлю там, где важно по смыслу (если не забываю).

    -
    -

    Почему у User.registered тип Date, а Meal.dateTime LocalDateTime?

    -
    -

    Время регистрации - абсолютное (конкретный момент), а время еды по бизнес логике относительно (те не зависит от часового пояса)

    -

    video 3. HW0 Optional 2: реализация в один проход и одной строкой Stream API

    -

    Apply 1_3_HW0_optional2.patch

    -

    Занятие 1:

    -

    video 4. Обзор используемых в проекте технологий. Интеграция ПО.

    - -

    video 5. Maven.

    - -

    video 6. WAR. Веб-контейнер Tomcat. Сервлеты.

    -
    -
      -
    • Нам нужна 8-я версия Tomcat (например последняя 8.5.16)
    • -
    • Устанавливать Tomcat лучше простым скачиванием архива apache-tomcat-8.5.16[-windows-xxx].zip и копированием из него в каталог без пробелов и русских букв (пример С:\java\apache-tomcat-8.5.16). Не используйте Tomcat 8.5.13 и 9.0.0.M19 IDEA из-за бага Tomcat
    • -
    -
    -

    Apply 1_4_switch_to_war.patch

    -
    -
      -
    • Обновил сервлеты до версии 3.1 -(Tomcat 8 использует это API, хотя для нас не принципиально, т.к. мы никакие фичи 3.x Servlet API не используем)
    • -
    • Переименовал userList.jsp в users.jsp
    • -
    • Сервлет добавляется в следующем патче, те в web.xml он будет подчеркиваться красным.
    • -
    -
    -

    Apply 1_5_add_servlet_api.patch

    -
    - -
    -

    Apply 1_6_forward_to_redirect.patch

    - -
    <user username="tomcat" password="tomcat" roles="tomcat,manager-gui,admin-gui"/>
    -
    - -

    video 7. Логирование.

    -

    Apply 1_7_logging.patch

    -
    -

    Установите переменную окружения на TOPJAVA_ROOT на корень проекта и перезапустите IDEA. Слеши в пути должны быть в стиле unix (/)

    -
    -
    -
      -
    • изменения в проекте: убрал LoggerWrapper и логирую напрямую в логгер SLF4J. При логгировании через вспомогательный класс, в логе теряется имя исходного класса.
    • -
    • удалил зависимость jul-to-slf4j. Она нам не нужна и, согласно видео Владимира Красильщика про логирование, она замедляет работу
    • -
    • Не делать конкатенацию строк при логгировании сообщений, если уровень логирования в конфигурации выставлен выше уровня логирования в коде - -
    • -
    -
    - -

    Проверочные вопросы:

    -
      -
    • Что нужно изменить в pom.xml, чтобы перейти с logback на log4j ?
    • -
    -

    question Ваши вопросы

    -
    -

    Почему private static final Logger log а не LOG ?

    -
    - -
    -

    Используются ли еще где-то в реальной разработке JSP, или это уже устаревшая технология? Заменит ли ее JSF (https://javatalks.ru/topics/38037)?

    -
    -

    JSF и JSP- разные ниши и задачи. -JSP- шаблонизатор, JSF- МVС фреймворк. Из моего опыта- с JSP сталкивался в 60% проектов. Его прямая замена: http://www.thymeleaf.org (в Spring-Boot по умолчанию), но в уже запущенных проектах встречается достаточно редко. JSP не умирает, потому что просто и дешево. Кроме того включается в большинство веб-контейнеров (в Tomcat его реализация Jasper)

    -

    JSF- sun-овский еще фреймворк, с которым я ни разу не сталкивался и особого желания нет. Вот он как раз, по моему мнению, активно замещается хотя бы javascript фреймворками (angular, ember, react).

    -
    -

    А зачем мы использовали logback? Почему SLF4J нас не устроило? Почему реализация логирования не log4j?

    -
    -

    SLF4J-API это API. Там есть только пустая реализация org.slf4j.helpers.NOPLogger (можно посмотреть в исходниках). Logback для новых проектов стал стандарт. spring-petclinic и spring-boot используют его по умолчанию.

    - -
    -

    hw Домашнее задание HW01 (делаем ветку HW01 от последнего патча в master)

    -
    -

    На проекте мы учимся мыслить и работать как Java разработчики уже сейчас, потом это будет гораздо сложнее и стоить дороже. -Вот на мой взгляд хорошие советы новичкам. От себя я добавлю:

    -
      -
    • Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В процессе формулирования очень часто приходит ее решение.
    • -
    • Учись исследовать проблему. Внимательное чтение логов и умение дебажить - основные навыки разработчика. Обычно самый верх самого нижнего эксепшена- причина ошибки, туда нужно ставить брекпойнт.
    • -
    • Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и биться над ней часами. Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время.
    • -
    -
    -

    1. Реализовать сервлет с отображением в таблице списка еды (в памяти и БЕЗ учета пользователя)

    -
    -

    Деплоиться в Tomcat лучше как war exploded: нет упаковки в war и при нажатой кнопке Update Resources on Frame Deactivation можно обновляться css, html, jsp без передеплоя. При изменении web.xml, добавлении методов, классов необходим redeploy.

    -
    -
      -
    • 1.1 По аналогии с UserServlet добавить MealServlet и meals.jsp -
        -
      • Задеплоить приложение (war) в Tomcat c applicationContext=topjava (приложение должно быть доступно по http://localhost:8080/topjava)
      • -
      • Попробовать разные деплои в Tomcat, remote и local debug
      • -
      -
    • -
    • 1.2 Сделать отображения списка еды в JSP, цвет записи в таблице зависит от параметра exceed (красный/зеленый). -
        -
      • 1.2.1 Список еды захардкодить (те проинициализировать в коде, желательно чтобы в проекте инициализация была только в одном месте)
      • -
      • 1.2.2 Время выводить без 'T'
      • -
      • 1.2.3 Список выводим БЕЗ фильтрации по startTime/endTime
      • -
      • 1.2.4 Вариант реализации: -
          -
        • из сервлета преобразуете еду в List<MealWithExceeded>;
        • -
        • кладете список в запрос (request.setAttribute);
        • -
        • делаете forward на jsp для отрисовки таблицы (при redirect атрибуты теряются).
        • -
        • в JSP для цикла можно использовать JSTL tag forEach. Для подключения JSTL в pom.xml и шапку JSP нужно добавить:
        • -
        -
      • -
      -
    • -
    -
        <dependency>
    -       <groupId>javax.servlet</groupId>
    -       <artifactId>jstl</artifactId>
    -       <version>1.2</version>
    -    </dependency>
    -
    -    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    -    ...
    -
    - -

    Optional

    -

    2. Реализуем в ПАМЯТИ CRUD (create/read/update/delete) для еды

    -

    Пример: Simple CRUD using Servlet/JSP

    -
      -
    • 2.1 Хранение в памяти будет одна из наших CRUD реализаций (позже будет JDBC, JPA и DATA-JPA).
    • -
    • 2.2 Работать с реализацией CRUD через интерфейс, который не должен ничего знать о деталях реализации (Map, DB или что-то еще).
    • -
    • 2.3 Добавить поле id в Meal/ MealWithExceed и реализовать генерацию счетчика, УЧЕСТЬ МНОГОПОТОЧНОСТЬ СЕРВЛЕТОВ - -
    • -
    • 2.4 Сделать форму редактирования в JSP: AJAX/JavaScript использовать НЕ надо, делаем через <form method="post"> и doPost() в сервлете.
    • -
    • 2.5 Для ввода дат и времени можно использовать html5 типы, хотя они поддерживаются не всеми браузерами (протестировать свой браузер). В конце курса мы добавим DateTimePicker jQuery plugin, который будет работать на всех браузерах.
    • -
    -

    После выполнения ДЗ обязательно проверьте решение на ошибки

    -

    question Вопросы по HW1

    -
    -

    Не попадаю на страничку/брекпойнт в сервлете.

    -
    -
      -
    • внимательно проверь url и applicationContext (Application Context должен быть тот же, что и url приложения: wiki IDEA)
    • -
    • посмотрите в task manager: возможно запущено несколько JVM и они мешают друг другу. Лишние java приложения убить.
    • -
    -
    -

    Приложение не видит TOPJAVA_ROOT.

    -
    -

    После выставления переменной окружения IDEA нужно рестартовать. Слеши в пути должны быть в стиле unix (/). Проверить, видит ли java переменную окружения можно так: System.getenv("TOPJAVA_ROOT"). Еще вариант: добавить -DTOPJAVA_ROOT=... в опции запуска приложения, тогда она доступна из java как System.getProperty("TOPJAVA_ROOT").

    -
    -

    Проблемы с кодировкой в POST (кракозябры).

    -
    -

    Возможное решение - выставьте кодировку ДО первого чтения из request:

    -
    protected void doPost(HttpServletRequest request, ...) {
    +Возможное решение - выставьте кодировку ДО первого чтения из request:
    +```
    +protected void doPost(HttpServletRequest request, ...) {
         request.setCharacterEncoding("UTF-8");
    -
    -
    -

    Если сервлет тыкают несколько пользователей / несколько браузеров, какого должно быть поведение? Нужно ли что-то делать с сессиями?

    -
    -

    В Optional нужно делать реализацию хранения многопоточной. Cессии пока не используем (начнутся, когда будет прикручивать авторизацию).

    -
    -

    Для чего нам нужна многопоточная реализация коллекции, если каждый пользователь видит только себя?

    -
    -

    Реализация хранения в памяти у нас одна на всех. Те коллекция шарится между пользователями, а они в разных потоках ее дергают. Если несколько потоков одновременно будут изменять коллекцию без учета многопоточности (например один будет удалять, второй вставлять), мы получим ConcurrentModificationException

    -
    -

    Предпочтительнее ли создавать новый объект Meal при каждом update?

    -
    -

    Если при обновлении не создавать копию, то сохраненный в памяти объект может кто-то попортить. Вопрос скорее доверия к коду- если проект большой и людей над ним трудится много, то вероятнее нужно копировать.

    -
    -

    Почему теряются атрибуты при передаче на сервлет: http://localhost:8080/topjava/meals?action=add&... и req.getAttribute("action") = null ?

    -
    -

    См. Difference between getAttribute() and getParameter(). Отсюда также следует, что при редиректе атрибуты теряются.

    -
    -

    Зачем нужен в jsp <jsp:useBean id=".." scope="request" type=".."/> ?

    -
    -

    jsp:useBean нужен IDEA для автодополнений - она понимает тип переменной, которая уже доступна в JSP (например через setAttribute). И еще эта переменная становится доступной в java вставках. Для вывода в JSP это тэг не обязателен. Если тип переменной JSP не совпадает с тем, что в jsp:useBean, будет ошибка.

    -
    -

    error Типичные ошибки

    -
      -
    • 1 Если в названии класса есть Meal, не нужно использовать слово meal в методах класса.
    • -
    • 2 Привыкайте писать комментарии к чекину: одной фразой что вы сделали в нем. Например: Meals CRUD implementation. См. -Как писать сообщения коммитов
    • -
    • 3 Хранение в памяти и операции с ней должны выполняться просто и эффективно
    • -
    • 4 Хранить нужно Meal и конвертировать ее в MealWithExceed когда отдаем список на отображение в JSP. Иначе при редактировании любой записи или изменении юзером своей нормы caloriesPerDay нужно будет пересчитывать все записи юзера.
    • -
    • 5 Стили color можно применять ко всей строке таблицы tr, а не каждой ячейке.
    • -
    • 6 DateTimeFormatter можно сделать один заранее (он потокобезопасный в отличие от SimpleDateFormatter), а не создавать новый при каждом запросе.
    • -
    • 7 Работать с CRUD надо через интерфейс.
    • -
    • 8 Реализаций хранения будет несколько, нужно учитывать это в названии класса имплементации работы в памяти.
    • -
    • 9 В web.xml принято группировать сервлет со своим маппингом
    • -
    • 10 Не размещайте никакую логику (форматирование, счетчики) в бинах, где хранятся только данные (Meal, MealWithExceed)
    • -
    • 11 Еще раз: детали реализации в памяти не должны быть никому видны. Те НЕ НАДО счетчик размещать в Meal или MealServlet или MealsUtil, в базе же он будет по другому генерится.
    • -
    • 12 volatile при ++ не помогает от многопоточности. Почему?
    • -
    • 13 Обратите также внимание на то, чтобы реализация вашей коллекции для хранения еды была также многопоточной.
    • -
    • 14 Не делайте дублирование кода MealsUtil. Возможно вам пригодятся константы LocalTime.MIN и LocalTime.MAX
    • -
    • 15 Не дублируйте строки в jsp. Посмотрите на тернарный оператор.
    • -
    • 16 после операции delete в браузере должен быть url http:\\localhost:8080\topjava\meals
    • -
    • 17 Перед чекином проверяйте свой ченджлист (Ctrl+D на файле из Local Changes - посмотреть что поменялось). Если там только пробелы/переводы строк, не надо его комитить - делайте файлу Git->revert.
    • -
    • 18 Учтите в названии реализации CRUD, что -
        -
      • 18.1 у нас будет несколько реализаций (не только в памяти)
      • -
      • 18.2 у нас будет 2 CRUD (для еды и пользователей). А в реальном проекте их намного больше.
      • -
      -
    • -
    • 19 Сессии НЕ использовать! При редиректе все атрибуты (req.getAttribute()) теряются (см. вопрос выше). Сценарий редиректа: -
        -
      • 1 из сервлета делаем редирект (снова на сервлет, не на JSP!)
      • -
      • 2 снова заходим в сервлет
      • -
      • 3 кладем нужные атрибуты и делаем forward на jsp
      • -
      • 4 если очень хочется передать параметры из 1. в 2. можно сделать их через параметры запроса (например meals?id=5) и доставать через reg.getParameter(id). В моей реализации такого не потребовалось.
      • -
      -
    • -
    -
    -
    - -
    - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +``` + +> Если сервлет тыкают несколько пользователей / несколько браузеров, какого должно быть поведение? Нужно ли что-то делать с сессиями? + +В Optional нужно делать реализацию хранения многопоточной. Cессии пока не используем (начнутся, когда будет прикручивать авторизацию). + +> Для чего нам нужна многопоточная реализация коллекции, если каждый пользователь видит только себя? + +Реализация хранения в памяти у нас одна на всех. Те коллекция шарится между пользователями, а они в разных потоках ее дергают. Если несколько потоков одновременно будут изменять коллекцию без учета многопоточности (например один будет удалять, второй вставлять), мы получим `ConcurrentModificationException` + +> Предпочтительнее ли создавать новый объект `Meal` при каждом update? + +Если при обновлении не создавать копию, то сохраненный в памяти объект может кто-то попортить. Вопрос скорее доверия к коду- если проект большой и людей над ним трудится много, то вероятнее нужно копировать. + +> Почему теряются атрибуты при передаче на сервлет: `http://localhost:8080/topjava/meals?action=add&...` и `req.getAttribute("action")` = null ? + +См. Difference between getAttribute() and getParameter(). Отсюда также следует, что при редиректе атрибуты теряются. + +> Зачем нужен в jsp `` ? + +jsp:useBean нужен IDEA для автодополнений - она понимает тип переменной, которая уже доступна в JSP (например через setAttribute). И еще эта переменная становится доступной в java вставках. Для вывода в JSP это тэг не обязателен. Если тип переменной JSP не совпадает с тем, что в `jsp:useBean`, будет ошибка. + +---------------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Типичные ошибки +- 1 **Если в названии класса есть `Meal`, не нужно использовать слово meal в методах класса.** +- 2 Привыкайте писать комментарии к чекину: одной фразой что вы сделали в нем. Например: *Meals CRUD implementation*. См. +[Как писать сообщения коммитов](http://frontiermag.ru/commit-message.html) +- 3 Хранение в памяти и операции с ней должны выполняться просто и эффективно +- 4 Хранить нужно `Meal` и конвертировать ее в `MealWithExceed` когда отдаем список на отображение в JSP. Иначе при редактировании любой записи или изменении юзером своей нормы `caloriesPerDay` нужно будет пересчитывать все записи юзера. +- 5 Стили `color` можно применять ко всей строке таблицы `tr`, а не каждой ячейке. +- 6 `DateTimeFormatter` можно сделать один заранее (он потокобезопасный в отличие от `SimpleDateFormatter`), а не создавать новый при каждом запросе. +- 7 Работать с CRUD надо через интерфейс. +- 8 Реализаций хранения будет несколько, нужно учитывать это в названии класса имплементации работы в памяти. +- 9 В `web.xml` принято группировать сервлет со своим маппингом +- 10 Не размещайте никакую логику (форматирование, счетчики) в бинах, где хранятся только данные (`Meal, MealWithExceed`) +- 11 Еще раз: детали реализации в памяти не должны быть никому видны. Те НЕ НАДО счетчик размещать в `Meal` или `MealServlet` или `MealsUtil`, в базе же он будет по другому генерится. +- 12 `volatile` при ++ не помогает от многопоточности. Почему? +- 13 Обратите также внимание на то, чтобы реализация вашей коллекции для хранения еды была также многопоточной. +- 14 Не делайте дублирование кода `MealsUtil`. Возможно вам пригодятся константы `LocalTime.MIN` и `LocalTime.MAX` +- 15 Не дублируйте строки в `jsp`. Посмотрите на тернарный оператор. +- 16 после операции `delete` в браузере должен быть url `http:\\localhost:8080\topjava\meals` +- 17 Перед чекином проверяйте свой ченджлист (`Ctrl+D` на файле из `Local Changes` - посмотреть что поменялось). Если там только пробелы/переводы строк, не надо его комитить - делайте файлу `Git->revert`. +- 18 Учтите в названии реализации CRUD, что + - 18.1 у нас будет несколько реализаций (не только в памяти) + - 18.2 у нас будет 2 CRUD (для еды и пользователей). А в реальном проекте их намного больше. +- 19 Сессии НЕ использовать! При редиректе все атрибуты (`req.getAttribute()`) теряются (см. вопрос выше). Сценарий редиректа: + - 1 из сервлета делаем редирект (снова на сервлет, не на JSP!) + - 2 снова заходим в сервлет + - 3 кладем нужные атрибуты и делаем forward на jsp + - 4 если очень хочется передать параметры из 1. в 2. можно сделать их через параметры запроса (например `meals?id=5`) и доставать через `reg.getParameter(id)`. В моей реализации такого не потребовалось. \ No newline at end of file diff --git a/doc/lesson02.md b/doc/lesson02.md index 8c0e7732f9f6..3028b689dc4d 100644 --- a/doc/lesson02.md +++ b/doc/lesson02.md @@ -1,1037 +1,238 @@ +# Онлайн проект Topjava +## Материалы занятия (скачать все патчи можно через Download папки patch) +### ![correction](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правки в проекте +#### Apply 2_0_add_fix.patch +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW1: +**Перед сборкой проекта (или запуском Tomcat)** +- **откройте вкладку Maven Projects и сделайте `clean`** +- **если страничка в браузере работает неверно, очистите кэш (`Ctrl+F5` в хроме)** +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Отображения списка еды в JSP +#### Apply 2_1_HW1.patch - - - - - - - - - - - +> - Изменения в `MealsUtil`: +> - Сделал константу `List MEALS` +> - Сделал вспомогательный метод `getWithExceeded` +> - Форматирование даты сделал на основе JSTL LocalDateTime format +> - Переименовал `TimeUtil` в `DateTimeUtil` +> - Переименовал `mealList.jsp` в `meals.jsp` +> - Добавил еще один способ вывести `dateTime` через стандартную JSTL функцию `replace` (префикс `fn` в шапке также надо поменять) +- [jsp:useBean](http://www.java2ee.ru/jsp/useBean.html) +- [MVC - Model View Controller](http://design-pattern.ru/patterns/mvc.html) - - - - - - - - - - topjava/lesson02.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - +## Занятие 2: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava +- Apache Commons, Guava + - Guava используется на проекте [Многомодульный maven. Многопоточность. XML (JAXB/StAX). Веб сервисы (JAX-RS/SOAP). Удаленное взаимодействие (JMS/AKKA)](http://javaops.ru/reg/masterjava) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Слои приложения. Создание каркаса приложения. +#### Apply 2_3_app_layers.patch +> - Переименовал `ExceptionUtil` в `ValidationUtil` +> - Поменял `LoggedUser` на `AuthorizedUser` +> - Добавил проверку id пользователя, пришедшего в контроллер ([treat IDs in REST body](https://stackoverflow.com/a/32728226/548473), "If it is a public API you should be conservative when you reply, but accept liberally") - +![Слои приложения](http://4.bp.blogspot.com/-B4BdrPHfILA/Tu6dC5uK4dI/AAAAAAAAAeQ/iPEM0WctR7Y/s1600/Roo+Technical+Architecture+Diagram.png) -
    +- Паттерн "Слои приложения" +- Data Access Object +- Паттерн DTO +- Value Object и Data Transfer Object +- Should services always return DTOs, or can they also return domain models? +- Дополнительно: + - Паттерны Repository и DAO + - Забудьте о DAO, используйте Repository + - Difference between Active Record and DAO -
    +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Какова цель деления приложения на слои? -
    -
    +Управляемость проекта (особенно большого) повышается на порядок: +- Обеспечивается меньшая связываемость. Допустим если мы меняем что-то в контроллере, то сервис эти изменения не задевают. +- Облегчается тестирование (мы будем тестировать слои сервисов и контроллеров отдельно) +> DTO используются когда есть избыточность запросов, которую мы уменьшаем, собрав данные из разных бинов в один? Когда DTO необходимо использовать? +(D)TO может быть как частью одного entity (набор полей) так и набором нескольких entities. +В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например `MealsWithExceeded` мы отдаем наружу и он является Transfer Object, его надо перенести в пакет `to`. +На многих проектах (и собеседованиях) практикуют разделение на уровне maven модулей entity слоя от логики и соответствующей конвертацией ВСЕХ Entity в TO, даже если у них те же самые поля. +Хороший ответ когда TO обязательны есть на stackoverflow: When to Use. -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink +> Почему контроллеры положили в папку web, а не в conrollers? - +То же самое что `domain/model` - просто разные названия. -
    - -
    - +> Зачем мы наследуем `NotFoundException` от `RuntimeException`? -
    +Так с ним удобнее работать. И у нас нет никаких действий по восстановлению состояния приложения (no recoverable conditions): checked vs unchecked exception. По последним данным checked exception вообще depricated: Ignore Checked Exceptions -
    -
    - - Switch branches/tags -
    +> Зачем в API пишем `NotFoundException`, если они `RuntimeException`? -
    -
    - -
    -
    - -
    -
    +Обычно не пишут. Я написал для информации разработчикам - здесь делаем проверку и может быть брошено. - +> Что лучше возвращать из API: `Collection` или `List` -
    -
    +Вообще, как правило, возвращают `List`, если не просится по коду более общий случай (например возможный `Set` или `Collection`, возвращаемый `Map.values()`). Если возвращается отсортированный список, то `List` будет адекватнее. +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Обзор Spring Framework. Spring Context. +#### Apply 2_4_add_spring_context.patch +- Spring Framework +- Проекты Spring. +- Обзор Spring Framework -
    +#### Apply 2_5_add_dependency_injection.patch +- Инверсия управления. +- IoC, DI, IoC-контейнер — Просто о простом -
    Nothing to show
    -
    +#### Apply 2_6_add_annotation_processing.patch +- Difference + between @Component, @Repository & @Service annotations in Spring +- Spring Auto Scanning Components +- Использование аннотации @Autowired +- Дополнительное: + - Introduction to the Spring IoC container + and beans + - Constructor против Setter Injection + - Getting Started + - Spring Framework Reference Documentation + - Spring на GitHub + - Spring Annotations -
    -
    -
    +#### Apply 2_7_constructor_injection.patch +- [Inject 2 beans of same type](https://stackoverflow.com/a/2153680/548473) +- [Перевод "Field Dependency Injection Considered Harmful"](https://habrahabr.ru/post/334636/) +- [Tutorial: testing with AssertJ](http://www.vogella.com/tutorials/AssertJ/article.html) +- [Field vs Constructor vs Setter DI](http://stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it) +- [Implicit constructor injection for single-constructor scenarios](https://spring.io/blog/2016/03/04/core-container-refinements-in-spring-framework-4-3#implicit-constructor-injection-for-single-constructor-scenarios) -
    - - Find file - - -
    - -
    +В контроллерах *Constructor Injection* делать не стал, добавляется лишний код (попробуйте сделать сами). На каждом проекте свои правила, универсальных нет. +#### Дополнительно видео по Spring + - [Юрий Ткач: Spring Framework - The Basics](https://www.youtube.com/playlist?list=PL6jg6AGdCNaWF-sUH2QDudBRXo54zuN1t) + - [Java Brains: Spring Framework](https://www.youtube.com/playlist?list=PLC97BDEFDCDD169D7) + - [Тимур Батыршинов: Spring](https://www.youtube.com/playlist?list=PLwwk4BHih4fho6gmaAwdHYZ6QQq0aE7Zi) +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Пояснения к HW2. Обработка Autowired -
    - - - bf0ccc5 - - Jan 9, 2018 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    - -
    - -
    - 239 lines (179 sloc) - - 30.1 KB -
    -
    - - -
    -

    Онлайн проект Topjava

    -

    Материалы занятия (скачать все патчи можно через Download папки patch)

    -

    correction Правки в проекте

    -

    Apply 2_0_add_fix.patch

    -

    hw Разбор домашнего задания HW1:

    -

    Перед сборкой проекта (или запуском Tomcat)

    -
      -
    • откройте вкладку Maven Projects и сделайте clean
    • -
    • если страничка в браузере работает неверно, очистите кэш (Ctrl+F5 в хроме)
    • -
    -

    video 1. Отображения списка еды в JSP

    -

    Apply 2_1_HW1.patch

    -
    -
      -
    • Изменения в MealsUtil: -
        -
      • Сделал константу List<Meal> MEALS
      • -
      • Сделал вспомогательный метод getWithExceeded
      • -
      -
    • -
    • Форматирование даты сделал на основе JSTL LocalDateTime format
    • -
    • Переименовал TimeUtil в DateTimeUtil
    • -
    • Переименовал mealList.jsp в meals.jsp
    • -
    • Добавил еще один способ вывести dateTime через стандартную JSTL функцию replace (префикс fn в шапке также надо поменять)
    • -
    -
    - -

    video 2. Optional: реализация CRUD

    -

    Apply 2_2_HW1_optional.patch

    -

    Про использование паттерна Repository будет подробно рассказано в видео "Слои приложения"

    -
    -
      -
    • Переименовал mealEdit.jsp в mealForm.jsp
    • -
    • В meal.jsp используется параметр запроса param.action, который не кладется в атрибуты.
    • -
    • В MealServlet.doGet() сделал выбор через switch
    • -
    • Пофиксил багу c history.back() в mealForm.jsp для FireFox (коммит формы при Cancel, сделал type="button").
    • -
    -
    -

    Дополнительно:

    - -

    question Вопросы по HW1

    -
    -

    Зачем в InMemoryMealRepositoryImpl наполнять map с помощью нестатического блока инициализации, а не в конструкторе?

    -
    -

    Разницы нет. Сделал чтобы напомнить вам про эту конструкцию. Малоизвестные особенности Java

    -
    -

    Почему InMemoryMealRepositoryImpl не singleton?

    -
    -

    Начиная с Servlet API 2.3 пул сервлетов не создается, создается только один инстанс сервлетов. Те. InMemoryMealRepositoryImpl в нашем случае создается тоже только один раз. Далее все наши классы слоев приложения будут создаваться через Spring, бины которого по умолчанию являются синглтонами (в его контексте).

    -
    -

    Objects.requireNonNull в MealServlet.getId(request) если у нас нет id в запросе бросает NPE (NullPointerException). Но оно вылетит и без этого метода. Зачем он нужен и почему мы его не обрабатываем?

    -
    -

    Objects.requireNonNull - это проверка предусловия (будет подробно на 4-м занятии). Означает что в метод пришел неверный аргумент (должен быть не null) и приложение сообщает об ошибке сразу на входе (а не "может быть где-то потом"). См. What is the purpose of Objects#requireNonNull. Если ее проглатывать или замазывать, то приложение возможно где-то работает неверно (приходят неверные аргументы), а мы об этом не узнаем. Красиво обрабатывать ошибки будем на последних занятиях (Spring Exception Handling).

    -

    Занятие 2:

    -

    video 3. Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava

    - -

    video 4. Слои приложения. Создание каркаса приложения.

    -

    Apply 2_3_app_layers.patch

    -
    -
      -
    • Переименовал ExceptionUtil в ValidationUtil
    • -
    • Поменял LoggedUser на AuthorizedUser
    • -
    • Добавил проверку id пользователя, пришедшего в контроллер (treat IDs in REST body, "If it is a public API you should be conservative when you reply, but accept liberally")
    • -
    -
    -

    Слои приложения

    - -

    question Ваши вопросы

    -
    -

    Какова цель деления приложения на слои?

    -
    -

    Управляемость проекта (особенно большого) повышается на порядок:

    -
      -
    • Обеспечивается меньшая связываемость. Допустим если мы меняем что-то в контроллере, то сервис эти изменения не задевают.
    • -
    • Облегчается тестирование (мы будем тестировать слои сервисов и контроллеров отдельно)
    • -
    -
    -

    DTO используются когда есть избыточность запросов, которую мы уменьшаем, собрав данные из разных бинов в один? Когда DTO необходимо использовать?

    -
    -

    (D)TO может быть как частью одного entity (набор полей) так и набором нескольких entities. -В нашем проекте для данных, которые надо отдавать наружу и отличающихся от Entiy (хранимый бин), мы будем делать (Data) Transfer Object и класть в отдельный пакет to. Например MealsWithExceeded мы отдаем наружу и он является Transfer Object, его надо перенести в пакет to. -На многих проектах (и собеседованиях) практикуют разделение на уровне maven модулей entity слоя от логики и соответствующей конвертацией ВСЕХ Entity в TO, даже если у них те же самые поля. -Хороший ответ когда TO обязательны есть на stackoverflow: When to Use.

    -
    -

    Почему контроллеры положили в папку web, а не в conrollers?

    -
    -

    То же самое что domain/model - просто разные названия.

    -
    -

    Зачем мы наследуем NotFoundException от RuntimeException?

    -
    -

    Так с ним удобнее работать. И у нас нет никаких действий по восстановлению состояния приложения (no recoverable conditions): checked vs unchecked exception. По последним данным checked exception вообще depricated: Ignore Checked Exceptions

    -
    -

    Зачем в API пишем NotFoundException, если они RuntimeException?

    -
    -

    Обычно не пишут. Я написал для информации разработчикам - здесь делаем проверку и может быть брошено.

    -
    -

    Зачем в AdminRestController переопределяются методы родителя с вызовом тех же родительских?

    -
    -

    Сделано на будущее, мы будем работать с AdminRestController.

    -
    -

    И что такое ProfileRestController?

    -
    -

    Контроллер, где авторизованный пользователь будет работать со своими данными

    -
    -

    Что лучше возвращать из API: Collection или List

    -
    -

    Вообще, как правило, возвращают List, если не просится по коду более общий случай (например возможный Set или Collection, возвращаемый Map.values()). Если возвращается отсортированный список, то List будет адекватнее.

    -

    video 5. Обзор Spring Framework. Spring Context.

    -

    Apply 2_4_add_spring_context.patch

    - -

    Apply 2_5_add_dependency_injection.patch

    - -

    Apply 2_6_add_annotation_processing.patch

    - -

    Apply 2_7_constructor_injection.patch

    - -

    В контроллерах Constructor Injection делать не стал, добавляется лишний код (попробуйте сделать сами). На каждом проекте свои правила, универсальных нет.

    -

    Дополнительно видео по Spring

    - -

    video 6. Пояснения к HW2. Обработка Autowired

    -

    <context:annotation-config/> говорит спрингу при поднятии контекста обрабатывать @Autowired (добавляется в контекст спринга AutowiredAnnotationBeanPostProcessor). После того, как все бины уже в контексте постпроцессор через отражение инжектит все @Autowired зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке.

    -

    question Ваши вопросы

    -
    -

    Что такое схема в spring-app.xml xsi:schemaLocation и зачем она нужна

    -
    -

    XML схема нужна для валидации xml, IDEA делает по ней автозаполнение.

    -
    -

    Что означает для Spring

    -
    -
     <bean class="ru.javawebinar.topjava.service.UserServiceImpl">
    -     <property name="repository" ref="mockUserRepository"/>
    - </bean> ?
    -
    -

    Можно сказать так: создай и занеси в свой контекст экземпляр класса (бин) UserServiceImpl и заинжекть в его проперти из своего контекста бин mockUserRepository.

    -
    -

    Как биндинг происходит для @Autowired? Как поступать, если у нас больше одной реализации UserRepository?

    -
    -

    @Autowired инжектит по типу (т.е. ижектит класс который реализует UserRepository). Обычно он один. Если у нас несколько реализаций, Spring не поднимится и поругается - No unique bean. -В этом случае можно уточнить имя бина через @Qualifier. @Qualifier обычно добавляют только в случае нескольких реализаций.

    -
    -

    Почему нельзя сервлет помещать в Spring контекст?

    -
    -

    Сервлеты- это исключительно классы servlet-api (веб контейнера) и должны инстанциироваться и работать в нем. Те технически можно ( без init/destroy), но идеологически - неверно. Также НЕ надо работать с cервлетом из SpringMain.

    -
    -
      -
    • Еще раз смотрим на демо приложение и вникаем, что такое пользователь и его еда и что он может с ней сделать. Когда пользователь авторизуется в приложении, его id и норма калорий чудесным образом попадают в AuthorizedUser и в приложении мы может обращаемся к ним. Как они реально туда попадут будет в уроке 9 (Spring Security, сессия и куки)
    • -
    • Перед началом выполнения ДЗ (ели есть хоть какие-то сомнения) прочитайте ВСЕ ДЗ. Если вопросы остаются- то ВСЕ подсказки. Особенно этот пункт важный, когда будете делать реальное рабочее ТЗ.
    • -
    -

    hw Домашнее задание HW02

    -
      -
    • 1: переименовать MockUserRepositoryImpl в InMemoryUserRepositoryImpl и имплементировать по аналогии с InMemoryMealRepositoryImpl (список пользователей возвращать отсортированным по имени)
    • -
    • 2: сделать Meal extends AbstractBaseEntity, MealWithExceed перенести в пакет ru.javawebinar.topjava.to (transfer objects)
    • -
    • 3: изменить MealRepository/InMemoryMealRepositoryImpl: в одном общем хранилище хранится вся еда всех пользователей. Любой пользователь авторизуется и видит/модифицирует только свою еду. -
        -
      • 3.1: если по запрошенному id еда отсутствует или чужая, возвращать null/false (см. комментарии в UserRepository)
      • -
      • 3.2: реализовать хранение еды для каждого пользователя можно с добавлением поля userId в Meal ИЛИ без него (как нравится). Напомню, что репозиторий один и приложение может работать одновременно с многими пользователями.
      • -
      • 3.3: список еды возвращать отсортированным по времени, последние записи наверху
      • -
      • 3.4: атомарность операций не требуется (коллизии при одновременном изменении одного пользователя можно не учитывать)
      • -
      -
    • -
    • 4: Реализовать слои приложения для функциональности "еда". API контроллера должна удовлетворять все потребности демо приложения и ничего лишнего (см. демо). -
        -
      • Смотрите на реализацию слоя для user и делаете по аналогии! Если там что-то непонятно, не надо исправлять или делать по своему. Задавайте вопросы. Если действительно нужна правка- я сделаю и напишу всем.
      • -
      • 4.1: после авторизации (сделаем позднее), id авторизованного юзера будет попадать в AuthorizedUser.id(). Запрос попадает в контроллер, методы которого будут доступны снаружи по http, т.е. запрос можно будет сделать с ЛЮБЫМ id для еды -(не принадлежащем авторизированному пользователю). Нельзя позволять модифицировать/смотреть чужую еду.
      • -
      • 4.2: AuthorizedUser известен только на слое web (см. реализацию ProfileRestController). MealService можно тестировать без подмены логики авторизации, принимаем в методах сервиса и репозитория параметр userId: id владельца еды.
      • -
      • 4.3: если еда не принадлежит авторизированному пользователю или отсутствует, в MealServiceImpl бросать NotFoundException.
      • -
      • 4.4: конвертацию в MealWithExceeded можно делать как в слое web, так и в service (Mapping Entity->DTO: Controller or Service?)
      • -
      • 4.5: в MealServiceImpl постараться сделать в каждом методе только одни запрос к MealRepository
      • -
      • 4.6 еще раз: не надо в названиях методов повторять названия класса (Meal).
      • -
      -
    • -
    • 5: включить классы еды в контекст Spring (добавить аннотации) и вызвать из SpringMain любой метод MealRestController (проверить что Spring все корректно заинжектил)
    • -
    -

    Optional

    -
      -
    • 6: в MealServlet сделать инициализацию Spring, достать MealRestController из контекста и работать с едой через него (как в SpringMain). pom.xml НЕ менять, работаем со spring-context. Сервлет обращается к контролеру, контроллер вызывает сервис, сервис - репозиторий. -
        -
      • 6.1: учесть, что когда будем работать через Spring MVC, MealServlet удалим, те вся логика должна быть в контроллере
      • -
      -
    • -
    • 7: добавить в meals.jsp и MealServlet две отдельные фильтрации еды: по дате и по времени (см. демо)
    • -
    • 8: добавить выбор текущего залогиненного пользователя (имитация авторизации, сделать Select с двумя элементами со значениями 1 и 2 в index.html и AuthorizedUser.setId(userId) в UserServlet). -Настоящая атворизация будет через Spring Security позже.
    • -
    -
    -

    error Подсказки по HW02 (для проверки, сначала сделайте самостоятельно!)

    -
      -
    • 1: В реализации InMemoryUserRepositoryImpl -
        -
      • 1.1: getByEmail попробуйте сделать через stream
      • -
      • 1.2: delete попробуйте сделать за одно обращение к map (без containsKey)
      • -
      • 1.3: предусмотрите случай одинаковых User.name (порядок должен быть зафиксированным).
      • -
      -
    • -
    • 2: В реализации InMemoryMealRepositoryImpl -
        -
      • 2.1: В Meal, которая приходит в контроллер нет никакой информации о пользователе (еда приходит в контроллер БЕЗ user/userId). Она может быть только авторизованного пользователя, поэтому что-то сравнивать с ним никакого смысла нет. По id еды и авторизованному пользователю нужно проверить ее принадлежность.
      • -
      • 2.2: get\update\delete - следите, чтобы не было NPE (NullPointException может быть, если в хранилище отсутствует юзер или еда).
      • -
      • 2.3: Проверьте сценарий: авторизованный пользователь пробует изменить чужую еду (id еды ему не принадлежит).
      • -
      • 2.4: Фильтрацию по датам сделать в репозитории т.к. из базы будем брать сразу отфильтрованные по дням записи. Следите чтобы первый и последний день не были обрезаны, иначе сумма калорий будет неверная.
      • -
      • 2.5: Если запрашивается список и он пустой, не возвращайте NULL! По пустому списку можно легко итерироваться без риска NullPoinException.
      • -
      • 2.6: Не дублируйте код в getAll и метод с фильтрацией
      • -
      • 2.7: попробуйте учесть, что следующая реализация (сортировка, фильтрация) будет делаться прямо в базе данных
      • -
      -
    • -
    • 3: Проверьте, что удалили Meal.id, он уже есть в базовом BaseEntity
    • -
    • 4: Проверку isBetweenDate сделать в DateTimeUtil. Попробуйте использовать дженерики и объединить ее с isBetweenTime
    • -
    • 5: Реализация 'MealRestController' должен уметь обрабатывать запросы: -
        -
      • 5.1: Отдать свою еду (для отображения в таблице, формат List<MealWithExceed>), запрос БЕЗ параметров
      • -
      • 5.2: Отдать свою еду, отфильтрованную по startDate, startTime, endDate, endTime
      • -
      • 5.3: Отдать/удалить свою еду по id, параметр запроса - id еды. Если еда с этим id чужая или отсутствует - NotFoundException
      • -
      • 5.4: Сохранить/обновить еду, параметр запроса - Meal. Если обновляемая еда с этим id чужая или отсутствует - NotFoundException
      • -
      • 5.5: Сервлет мы удалим, а контроллер останется, поэтому возвращать List<MealWithExceed> надо из контроллера. И userId принимать в контроллере НЕЛЬЗЯ (иначе - для чего аторизация?). Подмену MIX/MAX для Date/Time также сделайте здесь.
      • -
      • 5.6: В REST при update принято передавать id (см. AdminRestController.update)
      • -
      • 5.7: Сделайте отдельный getAll без применения фильтра
      • -
      -
    • -
    • 6: Проверьте корректную обработку пустых значений date и time в контроллере
    • -
    • 7: Авторизированного пользователя берем из AuthorizedUser.id(), cм. ProfileRestController
    • -
    • 8: В MealServlet -
        -
      • 8.1: Закрывать springContext в сервлете грамотнее всего в HttpServlet.destroy(): если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглтон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно.
      • -
      • 8.2: Не храните параметры фильтра как члены класса сервлета, это не многопоточно! Один экземпляр сервлета используется всеми запросами на сервер, попробуйте дернуть его из 2х браузеров.
      • -
      -
    • -
    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +`` говорит спрингу при поднятии контекста обрабатывать `@Autowired` (добавляется в контекст спринга `AutowiredAnnotationBeanPostProcessor`). После того, как все бины уже в контексте постпроцессор через отражение инжектит все `@Autowired` зависимости. Будет подробнее в видео "Жизненный цикл Spring контекста" на следующем уроке. + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Что такое схема в spring-app.xml xsi:schemaLocation и зачем она нужна + +XML схема нужна для валидации xml, IDEA делает по ней автозаполнение. + +> Что означает для Spring + + + + ? + +Можно сказать так: создай и занеси в свой контекст экземпляр класса (бин) `UserServiceImpl` и заинжекть в его проперти из своего контекста бин `mockUserRepository`. + +> Как биндинг происходит для `@Autowired`? Как поступать, если у нас больше одной реализации `UserRepository`? + +`@Autowired` инжектит по типу (т.е. ижектит класс который реализует `UserRepository`). Обычно он один. Если у нас несколько реализаций, Spring не поднимится и поругается - `No unique bean`. + В этом случае можно уточнить имя бина через @Qualifier. `@Qualifier` обычно добавляют только в случае нескольких реализаций. + +> Почему нельзя сервлет помещать в Spring контекст? + +Сервлеты- это исключительно классы `servlet-api` (веб контейнера) и должны инстанциироваться и работать в нем. Те технически можно ( без `init/destroy`), но идеологически - неверно. Также НЕ надо работать с cервлетом из `SpringMain`. + +-------------------- +- **Еще раз смотрим на [демо приложение](http://topjava.herokuapp.com) и вникаем, что такое пользователь и его еда и что он может с ней сделать. Когда пользователь авторизуется в приложении, его id и норма калорий чудесным образом попадают в `AuthorizedUser` и в приложении мы может обращаемся к ним. Как они реально туда попадут будет в уроке 9 (Spring Security, сессия и куки)** +- **Перед началом выполнения ДЗ (ели есть хоть какие-то сомнения) прочитайте ВСЕ ДЗ. Если вопросы остаются- то ВСЕ подсказки**. Особенно этот пункт важный, когда будете делать реальное рабочее ТЗ. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW02 +- 1: переименовать `MockUserRepositoryImpl` в `InMemoryUserRepositoryImpl` и имплементировать по аналогии с `InMemoryMealRepositoryImpl` (список пользователей возвращать отсортированным по имени) +- 2: сделать `Meal extends AbstractBaseEntity`, `MealWithExceed` перенести в пакет `ru.javawebinar.topjava.to` (transfer objects) +- 3: изменить `MealRepository/InMemoryMealRepositoryImpl`: в одном общем хранилище хранится вся еда всех пользователей. Любой пользователь авторизуется и видит/модифицирует только свою еду. + - 3.1: если по запрошенному id еда отсутствует или чужая, возвращать `null/false` (см. комментарии в `UserRepository`) + - 3.2: реализовать хранение еды для каждого пользователя можно с добавлением поля `userId` в `Meal` ИЛИ без него (как нравится). Напомню, что репозиторий один и приложение может работать одновременно с многими пользователями. + - 3.3: список еды возвращать отсортированным по времени, последние записи наверху + - 3.4: атомарность операций не требуется (коллизии при одновременном изменении одного пользователя можно не учитывать) +- 4: Реализовать слои приложения для функциональности "еда". API контроллера должна удовлетворять все потребности демо приложения и ничего лишнего (см. [демо](http://topjava.herokuapp.com)). + - **Смотрите на реализацию слоя для user и делаете по аналогии! Если там что-то непонятно, не надо исправлять или делать по своему. Задавайте вопросы. Если действительно нужна правка- я сделаю и напишу всем.** + - 4.1: после авторизации (сделаем позднее), id авторизованного юзера будет попадать в `AuthorizedUser.id()`. Запрос попадает в контроллер, методы которого будут доступны снаружи по http, т.е. запрос можно будет сделать с ЛЮБЫМ id для еды + (не принадлежащем авторизированному пользователю). Нельзя позволять модифицировать/смотреть чужую еду. + - 4.2: `AuthorizedUser` известен только на слое web (см. реализацию `ProfileRestController`). `MealService` можно тестировать без подмены логики авторизации, принимаем в методах сервиса и репозитория параметр `userId`: id владельца еды. + - 4.3: если еда не принадлежит авторизированному пользователю или отсутствует, в `MealServiceImpl` бросать `NotFoundException`. + - 4.4: конвертацию в `MealWithExceeded` можно делать как в слое web, так и в service ([Mapping Entity->DTO: Controller or Service?](http://stackoverflow.com/questions/31644131)) + - 4.5: в `MealServiceImpl` постараться сделать в каждом методе только одни запрос к `MealRepository` + - 4.6 еще раз: не надо в названиях методов повторять названия класса (`Meal`). +- 5: включить классы еды в контекст Spring (добавить аннотации) и вызвать из `SpringMain` любой метод `MealRestController` (проверить что Spring все корректно заинжектил) + +### Optional +- 6: в `MealServlet` сделать инициализацию Spring, достать `MealRestController` из контекста и работать с едой через него (как в `SpringMain`). `pom.xml` НЕ менять, работаем со `spring-context`. Сервлет обращается к контролеру, контроллер вызывает сервис, сервис - репозиторий. + - 6.1: учесть, что когда будем работать через Spring MVC, `MealServlet` удалим, те вся логика должна быть в контроллере +- 7: добавить в `meals.jsp` и `MealServlet` две отдельные фильтрации еды: по дате и по времени (см. [демо](http://topjava.herokuapp.com)) +- 8: добавить выбор текущего залогиненного пользователя (имитация авторизации, сделать Select с двумя элементами со значениями 1 и 2 в `index.html` и `AuthorizedUser.setId(userId)` в `UserServlet`). +Настоящая атворизация будет через Spring Security позже. +--------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW02 (для проверки, сначала сделайте самостоятельно!) + +- 1: **В реализации `InMemoryUserRepositoryImpl`** + - 1.1: `getByEmail` попробуйте сделать через `stream` + - 1.2: `delete` попробуйте сделать за одно обращение к map (без `containsKey`) + - 1.3: предусмотрите случай одинаковых `User.name` (порядок должен быть зафиксированным). +- 2: **В реализации `InMemoryMealRepositoryImpl`** + - 2.1: В `Meal`, которая приходит в контроллер нет никакой информации о пользователе (еда приходит в контроллер БЕЗ `user/userId`). Она может быть только авторизованного пользователя, поэтому что-то сравнивать с ним никакого смысла нет. По `id` еды и авторизованному пользователю нужно проверить ее принадлежность. + - 2.2: `get\update\delete` - следите, чтобы не было NPE (`NullPointException` может быть, если в хранилище отсутствует юзер или еда). + - 2.3: Проверьте сценарий: авторизованный пользователь пробует изменить чужую еду (id еды ему не принадлежит). + - 2.4: Фильтрацию по датам сделать в репозитории т.к. из базы будем брать сразу отфильтрованные по дням записи. Следите чтобы **первый и последний день не были обрезаны, иначе сумма калорий будет неверная**. + - 2.5: Если запрашивается список и он пустой, не возвращайте NULL! По пустому списку можно легко итерироваться без риска `NullPoinException`. + - 2.6: Не дублируйте код в `getAll` и метод с фильтрацией + - 2.7: попробуйте учесть, что следующая реализация (сортировка, фильтрация) будет делаться прямо в базе данных +- 3: Проверьте, что удалили `Meal.id`, он уже есть в базовом `BaseEntity` +- 4: Проверку `isBetweenDate` сделать в `DateTimeUtil`. Попробуйте использовать дженерики и объединить ее с `isBetweenTime` +- 5: **Реализация 'MealRestController' должен уметь обрабатывать запросы**: + - 5.1: Отдать свою еду (для отображения в таблице, формат `List`), запрос БЕЗ параметров + - 5.2: Отдать свою еду, отфильтрованную по startDate, startTime, endDate, endTime + - 5.3: Отдать/удалить свою еду по id, параметр запроса - id еды. Если еда с этим id чужая или отсутствует - `NotFoundException` + - 5.4: Сохранить/обновить еду, параметр запроса - Meal. Если обновляемая еда с этим id чужая или отсутствует - `NotFoundException` + - 5.5: Сервлет мы удалим, а контроллер останется, поэтому возвращать `List` надо из контроллера. И userId принимать в контроллере НЕЛЬЗЯ (иначе - для чего аторизация?). Подмену `MIX/MAX` для `Date/Time` также сделайте здесь. + - 5.6: В REST при update принято передавать id (см. `AdminRestController.update`) + - 5.7: Сделайте отдельный `getAll` без применения фильтра +- 6: Проверьте корректную обработку пустых значений date и time в контроллере +- 7: Авторизированного пользователя берем из `AuthorizedUser.id()`, cм. `ProfileRestController` +- 8: В `MealServlet` + - 8.1: Закрывать springContext в сервлете грамотнее всего в `HttpServlet.destroy()`: если где-то в контексте Spring будет ленивая инициализация, метод-фабрика, не синглтон-scope, то контекст понадобится при работе приложения. У нас такого нет, но делать надо все грамотно. + - 8.2: Не храните параметры фильтра как члены класса сервлета, это не многопоточно! Один экземпляр сервлета используется всеми запросами на сервер, попробуйте дернуть его из 2х браузеров. \ No newline at end of file diff --git a/doc/lesson03.md b/doc/lesson03.md index 305e5594d92b..c73957089b5b 100644 --- a/doc/lesson03.md +++ b/doc/lesson03.md @@ -1,1067 +1,252 @@ +# Онлайн проект Topjava + +## [Материалы занятия](https://drive.google.com/drive/u/0/folders/0B9Ye2auQ_NsFT1NxdTFOQ1dvVnM) (скачать все патчи можно через Download папки patch) + +### ![correction](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правки в проекте +#### Apply 3_0_fix.patch +- Поправил `InMemoryMealRepositoryImpl.save()`. Если обновляется еда, которой нет в хранилище (c несуществующим id), вставка не происходит. См. также объяснение `computeIfPresent` в HW02 +- В местах, где требуется `int`, заменил `Integer.valueOf()` на `Integer.parseInt()` +- Закомментировал ненужный `context:annotation-config`: сканирование аннотаций подключаются при `context:component-scan` +- В поля ввода формы добавил `required` + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW02 +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW2 +> **ВНИМАНИЕ! При удалении класса из исходников, его скомпилированная версия все еще будет находиться в target (и classpath). В этом случае (или в любом другом, когда проект начинает глючить) сделайте `mvn clean`.** + +#### Apply 3_01_HW2_repository.patch +> - В репозиториях по другому инстанциировал компараторы. [Оптимизация анонимных классов](http://stackoverflow.com/questions/19718353) не требуется! Почитайте комменты от Holger: *Java 8 relieves us from the need to think about such things at all*. +> - Зарефакторил `> DateTimeUtil.isBetween(T value, T start, T end)`. Дженерики означают, что мы принимаем экземпляры класса, реализующего компаратор, который умеет сравнивать T или суперклассы от T +> - В `InMemoryMealRepositoryImpl.save()` вместо 2-х разнесенных по времени операций +> - `get(meal.getId(), userId)` +> - `meals.put(meal.getId(), meal)`, +между которыми может вклинится операция удаления этой еды из другого потока, сделал обновление атомарным, используя `ConcurrentHashMap.computeIfPresent()` (см. псевдокод в `Map.computeIfPresent`. `ConcurrentHashMap` в отличие от `HashMap` делает операции атомарно). + +#### Apply 3_02_HW2_meal_layers.patch +> - Перенес обработку null-дат в `MealRestController.getBetween()` +> - По аналогии с `AbstractUserController` добавил проверку id пользователя, пришедшего в `MealRestController (assureIdConsistent, checkNew)` + +#### Apply 3_03_HW2_optional_MealServlet.patch +> - Убрал логирование (уже есть в контроллере) +> - `assureIdConsistent` позволяет в контроллере обновлять еду с `id=null` + +#### Apply 3_04_HW2_optional_filter.patch +> - Вместо `MealServlet.resetParam` (перемещение параметров фильтрации в атрибуты запроса для отображения в `meals.jsp`), достаю их в jsp напрямую из запроса через [`${param.xxx}`](https://stackoverflow.com/a/1890462/548473) +> - В демо фильтр не хранится в сессии (скидывается по F5). Что такое сессия будем разбирать, когда будем делать реальную авторизацию + +#### Apply 3_05_HW2_optional_select_user.patch + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Вопросы по API и слоям приложения +- Should services always return DTOs, or can they also return domain models? +- Mapping Entity->DTO goes in which application layer: Controller or Service? + +### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы по HW2 + +> Что делает `repository.computeIfAbsent / computeIfPresent` ? + +Всегда пробуйте ответить на вопрос сами. Дастоточно просто зайти по Ctrl+мышка в реализацию и посмотреть javadoc и **их дефолтную реализацию** + +> Почему выбрана реализация `Map>` а не `Meal.userId + Map` ? + +В данном случае двойная мапа - самый эффективный способ хранения, который не требует итерирования (перебора всех значений). + +## Занятие 3: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Коротко о жизненном цикле Spring контекста. +#### Apply 3_06_bean_life_cycle.patch +- Spring изнутри. Этапы инициализации контекста. +- Ресурсы: + - Евгений Борисов. Spring, часть 1 + - Евгений Борисов. Spring, часть 2 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Тестирование через JUnit. +> **ВНИМАНИЕ!! Перед накаткой патча создайте каталог test (из корня проекта путь `\src\test`), иначе часть файлов попадет в `src\main`.** +> - в `maven-surefire-plugin` (JUnit) поменял кодировку на UTF-8 +> - добавил метод `InMemoryUserRepositoryImpl.init()` для инициализации. +> - `save()` не больше работает для отсутствующих `id` +> - автогенерацию id начал со 100 + +#### Apply 3_07_add_junit.patch +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: почему проект упадет при попытке открыть страничку еды (в логе смотреть самый верх самого нижнего исключения)? +- JUnit 4 +- Тестирование в Java. JUnit + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Spring Test +> - поменял `@RunWith`: `SpringRunner` is an alias for the `SpringJUnit4ClassRunner` +#### Apply 3_08_add_spring_test.patch +- Spring Testing + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Базы данных. Обзор NoSQL и Java persistence solution без ORM. +- PostgreSQL. +- [PostgreSQL JDBC Driver](https://github.com/pgjdbc/pgjdbc) +- Установка PostgreSQL. **ВНИМАНИЕ! с postgres 9.6 возможны проблемы.** +- Чтобы избежать проблем с правами и именами каталогов, [**рекомендуют установить postgres в простой каталог, например `C:\Postgresql`**. И при проблемах создать каталог data на другом диске](https://stackoverflow.com/questions/43432713/postgresql-installation-on-windows-8-1-database-cluster-initialisation-failed). Если Unix, [проверить права доступа к папке (0700)](http://www.sql.ru/forum/765555/permissions-should-be-u-rwx-0700). + +> Создать в pgAdmin новую базу `topjava` и новую роль `user`, пароль `password` + +![image](https://cloud.githubusercontent.com/assets/13649199/18809406/118f9c48-8283-11e6-8f10-d8291517a497.png) + +> Проверьте, что у user в Privileges есть возможность авторизации (особенно для pgAdmin4) + +- NoSQL or RDBMS. Обзор NoSQL систем. CAP +- DB-Engines Ranking +- JDBC +- Обзор Java persistence solution без ORM: commons-dbutils, + Spring JdbcTemplate, + MyBatis, JDBI, jOOQ +- Основы: + - Реляционная СУБД + - Реляционные базы + - Уроки по JDBC + - Postgres Guide + - PostgreSQL Tutorial + - Try SQL + - Базы данных на Java + - Возможности JDBC — второй этап +- Дополнительно: + - [Документация к PostgreSQL 9.6](https://postgrespro.ru/docs/postgresql/9.6/index.html) + - [Книги по PostgreSQL](https://postgrespro.ru/education/books) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Настройка Database в IDEA. +#### Apply 3_09_add_postgresql.patch +- Настройка Database в IDEA и запуск SQL. +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Скрипты инициализации базы. Spring Jdbc Template. +> в `JdbcUserRepositoryImpl.getByEmail()` заменил `queryForObject()` на `query()`. Загляните в код: `queryForObject` бросает `EmptyResultDataAccessException` вместо нужного нам `null`. +#### Apply 3_10_db_implementation.patch +- Подключение Spring Jdbc. +- Конфигурирование DataSource. Property Placeholder +> Проверьте, что в контекст Spring проекта включены оба файла конфигурации +![image](https://cloud.githubusercontent.com/assets/13649199/24730713/eb21456a-1a6d-11e7-997c-fb4ad728ba45.png) +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Тестирование UserService через AssertJ. +#### Apply 3_11_test_UserService.patch +- [Spring Testing Annotations](https://docs.spring.io/spring/docs/4.3.x/spring-framework-reference/htmlsingle/#integration-testing-annotations-spring) +- [The JPA hashCode() / equals() dilemma](https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma) +- [Hibernate: implementing equals() and hashCode()](https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#mapping-model-pojo-equalshashcode) +- [Junit Matcher for comparators](https://stackoverflow.com/questions/17949752) +- [AssertJ custom comparison strategy](http://joel-costigliola.github.io/assertj/assertj-core-features-highlight.html#custom-comparison-strategy). [AssertJ field by field comparisons](http://joel-costigliola.github.io/assertj/assertj-core-features-highlight.html#field-by-field-comparison) - - - - - - - - - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 10. Логирование тестов. +#### Apply 3_12_test_logging.patch +> - Новый PostgreSQL JDBC Driver [логирует через java.util.logging](https://github.com/pgjdbc/pgjdbc#changelog). [Направил логирование в SLF4J](http://stackoverflow.com/a/43242620/548473) +> - Поменял формат вывода. См. [Logback Layouts](https://logback.qos.ch/manual/layouts.html) +- Ресурсы, которые кладутся в classpath, maven при сборке берет из определенных каталогов `resources` ([Introduction to the Standard Directory Layout](https://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html)). Их можно настраивать через [maven-resources-plugin](https://maven.apache.org/plugins/maven-resources-plugin/examples/resource-directory.html), меняем в проекте Masterjava. +#### Apply 3_13_fix_servlet.patch +**Приложение перестало работать, тк. для репозитория мы используем заглушку `JdbcMealRepositoryImpl`** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 11. Ответы на Ваши вопросы +- Что такое REST? 10 Best Practices for Better RESTful API +- Зачем нужна сортировка еды? +- Можно ли обойтись без `MapSqlParameterSource`? +- Как происходит работа с DB в тестах? +- Как реализовывать RowMapper? +- Мои комментарии: решения проблем разработчиком. +- Нужен ли разработчику JavaScript? + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Какая разница между @BeforeClass and @Before? + +`@BeforeClass` выполняется один раз после загрузки класса (поэтому метод может быть только статический), `@Before` перед каждым тестом. +Также: для чистоты тестов экземпляр тестового класса пересоздается перед каждым тестом: http://stackoverflow.com/questions/6094081/junit-using-constructor-instead-of-before + +> Тесты в классе в каком-то определенном порядке выполняются ("сверху вниз" например)? + +Порядок по умолчанию неопределен, каждый тест должен быть автономен и не зависеть от других. См. также http://stackoverflow.com/questions/3693626/how-to-run-test-methods-in-specific-order-in-junit4 + +> Обязательно ли разворачивать postgreSQL? + +Желательно: хорошая и надежная ДБ:) Если совсем не хочется - можно работать со своей любимой RDBMS (поправить `initDB.sql`) или работать c postgresql в heroku (креденшелы к нему есть сверху в `postgres.properties`). На следующем уроке добавим HSQLDB, она не требует установки. + +> Зачем начали индексацию с 100000? - - - - - - - - - - topjava/lesson03.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - +Тут нет "как принято". Так удобно вставлять в базу (если будет потребность) записи вручную не мешая счетчику. + +> Из 5-го видео - "Логика в базе - большое зло". Можно чуть поподробней об этом? +- Есть успешные проекты с логикой в базе. Те все относительно. +- Логика в базе - это процедуры и триггеры. Нет никакого ООП, переиспользовать код достаточно сложно, никагого рефакторинга, поиска по коду и других плюшек IDE. Нельзя делать всякие вещи типа кэширования, хранения в сесии - это все для логики на стороне java. Например json можно напрямую отдать в процедуру и там парсить и вставлять в таблицы или наоборот - собирать из таблиц и возвращать. +А затем потребуется некоторая логика на стороне приложения и все равно придется этот json дополнительно разпарсивать в java. +Я на таком проекте делал специальную миграцию, чтобы процедуры мигрировать не как sql скрипты, а каждую процедуру хранить как класс с историей изменений. Если логика: триггеры и простые процедуры записи-чтения, которые не требуют переиспользования кода или +проект небольшой это допустимо, иначе проект становится трудно поддерживать. Также иногда используют [View](http://postgresql.ru.net/gruber/ch20.html) для разграничения доступа. Например, для финансовых систем, таблицы проводок доступны только для админ учеток, а View просто не дадут увидеть (тем более изменить) данны обычному оператору на уровне СУБД. - +> У JUnit есть ассерты и у спринга тоже. Можно ли обойтись без JUnit? - - +Предусловия и JUnit-тесты совершенно разные вещи. Один другого не заменит, у нас будут предусловия в следующем уроке. - - +> Я так понял VARCHAR быстрее, чем TEXT, когда мы работаем с небольшими записями. Наши записи будут небольшими (255). Почему вы приняли решение перейти на TEXT? - +В отличие от MySql в Postgres VARCHAR и TEXT - тоже самое: http://stackoverflow.com/questions/4848964/postgresql-difference-between-text-and-varchar-character-varying - +> Зачем при создании таблицы мы создаем `CREATE UNIQUE INDEX` и `CREATE INDEX`. При каких запросах он будет использоваться? - - +UNIQUE индекс нужен для обеcпечения уникальности, DB не даст сделать одинаковый индекс. Индексы используется для скорости выполнения запросов. Обычно они задействуются, когда в запросе есть условия, на которые сделан индекс. Узнать по конкретному запросу можно запросив план запроса: см. Оптимизация запросов. Основы EXPLAIN в PostgreSQL. На измерение производительности с индексами посмотрим в следующем уроке. - +> А это нормально, что у нас в базе у meals есть userId, а в классе - нет? - - +Что значит - "нормально"? Приложение работает. Ненормально, когда в приложении есть "лишний" код, который не используется. Для ORM он нам понадобится- мы `Meal.user` добавим. - +> Почему мы использует один sequence на разные таблицы? +Мы будем использовать Hibernate, по умолчанию он делает глобальный sequence на все таблицы. В этом подходе есть как плюсы, так и минусы, из плюсов - удобно делать ссылки в коде и в таблицах на при наследовании и мапы в коде. В дополнение: Configure Hibernate to create separate sequence for each table by default. - +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW03 +- 1 Понять, почему перестали работать `SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest` +- 2 Дополнить скрипты создания и инициализации базы таблицой MEALS. Запустить скрипты на вашу базу (через Run). Порядок таблиц при DROP и DELETE важен, если они связаны внешними ключами (foreign key, fk). Проверьте, что ваши скрипты работают +- 3 Реализовать через Spring JDBC Template `JdbcMealRepositoryImpl` + - 3.1. сделать каждый метод за один SQL запрос + - 3.2. `userId` в класс `Meal` вставлять НЕ надо (для UI и REST это лишние данные, userId это id залогиненного пользователя) + - 3.3. `JbdcTemplate` работает через сеттеры. Вместе с конструктором по умолчанию их нужно добавить в `Meal` + - 3.4. Cписок еды должен быть отсортирован (тогда мы его сможем сравнивать с тестовыми данными). Кроме того это требуется для UI и API: последняя еда наверху. +- 4 Проверить работу MealServlet, запустив приложение +#### Optional +- 5 Сделать `MealServiceTest` из `MealService` и реализовать тесты. +> По `Ctrl+Shift+T` (выбрать JUnit4) можно создать тест для конкретного класса, выбрав для него нужные методы. Тестовый класс создастся в папке `test` в том же пакете, что и тестируемый. + - 5.1 Сделать тестовые данные `MealTestData` (точно такие же, как вставляем в `populateDB.sql`). + - 5.2 Сделать тесты на чужую еду (delete, get, update) с тем чтобы получить `NotFoundException`. +- 6 Почнинить `SpringMain, InMemory*Test`. `InMemory*Test` **должны использовать реализацию в памяти** +- 7 Сделать индексы к таблице `Meals`: запретить создавать у одного и того-же юзера еду с одинаковой dateTime. +Индекс на pk (id) postgres создает автоматически: Postgres and Indexes on Foreign Keys and Primary Keys + - Postgres Guide: Indexes + - [Оптимизация запросов. Основы EXPLAIN в PostgreSQL](https://habrahabr.ru/post/203320/) + - [Оптимизация запросов. Часть 2](https://habrahabr.ru/post/203386/) + - [Оптимизация запросов. Часть 3](https://habrahabr.ru/post/203484/) - +> ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Как правильно придумать индекс для базы? Указать в нем все поля, комбинация которых создает по смыслу уникальную запись, или какие-то еще есть условия? - +Индекс нужно делать по тем полям, по которым будут искаться записи (участвуют в WHERE, ORDER BY). Уникальность - совсем не обязательное условие. Индексы ускоряют поиск по определенным полям таблицы. Они не бесплатные (хранятся в памяти, замедляется вставка), поэтому на всякий случай их делать не надо. Также не строят индексы на колонки с малым процентом уникальности (например поле "М/Ж"). Поля индекса НЕ КОММУТАТИВНЫ и порядок полей в описании индекса НЕОБХОДИМО соблюдать (в силу использования B-деревьев и их производных как поисковый механизм индекса). При построении плана запроса EXPLAIN учитывается количество записей в базе, поэтому вместо индексного поиска (Index Scan) база может выбрать последовательный (Seq Scan). Проверить, работают ли индексы можно отключив Seq Scan. Также см. Queries on the first field of composite index - - +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Решение проблем - +> Из каталога `main` не видятся классы/ресурсы в `test` +Все что находится в `test` используется только для тестов и недоступно в основном коде. - +> Из `IDEA` не видятся ресурсы в каталоге `test` - +- Сделайте Reimport All в Maven окне - - +![image](https://cloud.githubusercontent.com/assets/13649199/18831806/7e43bedc-83f0-11e6-97db-67d4e1a7599f.png) -
    - Skip to content -
    - - - - +> В UserServiceImpl и MealServiceImpl подчеркнуты красным repository, ошибка: Could not autowire. There is more than one bean of 'MealRepository' type. +- Spring test контекст не надо включать в Spring Facets проекта, там должны быть только `spring-app.xml` и `spring-db.xml`. Для тестовых контекстов поставьте чекбокс `Check test files` в Inspections. +![image](https://cloud.githubusercontent.com/assets/13649199/18831817/8a858f22-83f0-11e6-837e-bf5317b33b3a.png) - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - bf0ccc5 - - Jan 9, 2018 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    - -
    - -
    - 253 lines (184 sloc) - - 31.7 KB -
    -
    - - -
    -

    Онлайн проект Topjava

    -

    Материалы занятия (скачать все патчи можно через Download папки patch)

    -

    correction Правки в проекте

    -

    Apply 3_0_fix.patch

    -
      -
    • Поправил InMemoryMealRepositoryImpl.save(). Если обновляется еда, которой нет в хранилище (c несуществующим id), вставка не происходит. См. также объяснение computeIfPresent в HW02
    • -
    • В местах, где требуется int, заменил Integer.valueOf() на Integer.parseInt()
    • -
    • Закомментировал ненужный context:annotation-config: сканирование аннотаций подключаются при context:component-scan
    • -
    • В поля ввода формы добавил required
    • -
    -

    hw Разбор домашнего задания HW02

    -

    video 1. HW2

    -
    -

    ВНИМАНИЕ! При удалении класса из исходников, его скомпилированная версия все еще будет находиться в target (и classpath). В этом случае (или в любом другом, когда проект начинает глючить) сделайте mvn clean.

    -
    -

    Apply 3_01_HW2_repository.patch

    -
    -
      -
    • В репозиториях по другому инстанциировал компараторы. Оптимизация анонимных классов не требуется! Почитайте комменты от Holger: Java 8 relieves us from the need to think about such things at all.
    • -
    • Зарефакторил <T extends Comparable<? super T>> DateTimeUtil.isBetween(T value, T start, T end). Дженерики означают, что мы принимаем экземпляры класса, реализующего компаратор, который умеет сравнивать T или суперклассы от T
    • -
    • В InMemoryMealRepositoryImpl.save() вместо 2-х разнесенных по времени операций -
        -
      • get(meal.getId(), userId)
      • -
      • meals.put(meal.getId(), meal), -между которыми может вклинится операция удаления этой еды из другого потока, сделал обновление атомарным, используя ConcurrentHashMap.computeIfPresent() (см. псевдокод в Map.computeIfPresent. ConcurrentHashMap в отличие от HashMap делает операции атомарно).
      • -
      -
    • -
    -
    -

    Apply 3_02_HW2_meal_layers.patch

    -
    -
      -
    • Перенес обработку null-дат в MealRestController.getBetween()
    • -
    • По аналогии с AbstractUserController добавил проверку id пользователя, пришедшего в MealRestController (assureIdConsistent, checkNew)
    • -
    -
    -

    Apply 3_03_HW2_optional_MealServlet.patch

    -
    -
      -
    • Убрал логирование (уже есть в контроллере)
    • -
    • assureIdConsistent позволяет в контроллере обновлять еду с id=null
    • -
    -
    -

    Apply 3_04_HW2_optional_filter.patch

    -
    -
      -
    • Вместо MealServlet.resetParam (перемещение параметров фильтрации в атрибуты запроса для отображения в meals.jsp), достаю их в jsp напрямую из запроса через ${param.xxx}
    • -
    • В демо фильтр не хранится в сессии (скидывается по F5). Что такое сессия будем разбирать, когда будем делать реальную авторизацию
    • -
    -
    -

    Apply 3_05_HW2_optional_select_user.patch

    -

    video 2. Вопросы по API и слоям приложения

    - -

    question Вопросы по HW2

    -
    -

    Что делает repository.computeIfAbsent / computeIfPresent ?

    -
    -

    Всегда пробуйте ответить на вопрос сами. Дастоточно просто зайти по Ctrl+мышка в реализацию и посмотреть javadoc и их дефолтную реализацию

    -
    -

    Почему выбрана реализация Map<userId, Map<mealId,Meal>> а не Meal.userId + Map<mealId,Meal> ?

    -
    -

    В данном случае двойная мапа - самый эффективный способ хранения, который не требует итерирования (перебора всех значений).

    -

    Занятие 3:

    -

    video 3. Коротко о жизненном цикле Spring контекста.

    -

    Apply 3_06_bean_life_cycle.patch

    - -

    video 4. Тестирование через JUnit.

    -
    -

    ВНИМАНИЕ!! Перед накаткой патча создайте каталог test (из корня проекта путь \src\test), иначе часть файлов попадет в src\main.

    -
      -
    • в maven-surefire-plugin (JUnit) поменял кодировку на UTF-8
    • -
    • добавил метод InMemoryUserRepositoryImpl.init() для инициализации. -
        -
      • save() не больше работает для отсутствующих id
      • -
      • автогенерацию id начал со 100
      • -
      -
    • -
    -
    -

    Apply 3_07_add_junit.patch

    -

    question Вопрос: почему проект упадет при попытке открыть страничку еды (в логе смотреть самый верх самого нижнего исключения)?

    - -

    video 5. Spring Test

    -
    -
      -
    • поменял @RunWith: SpringRunner is an alias for the SpringJUnit4ClassRunner
    • -
    -
    -

    Apply 3_08_add_spring_test.patch

    - -

    video 6. Базы данных. Обзор NoSQL и Java persistence solution без ORM.

    - -
    -

    Создать в pgAdmin новую базу topjava и новую роль user, пароль password

    -
    -

    image

    -
    -

    Проверьте, что у user в Privileges есть возможность авторизации (особенно для pgAdmin4)

    -
    - -

    video 7. Настройка Database в IDEA.

    -

    Apply 3_09_add_postgresql.patch

    - -

    video 8. Скрипты инициализации базы. Spring Jdbc Template.

    -
    -

    в JdbcUserRepositoryImpl.getByEmail() заменил queryForObject() на query(). Загляните в код: queryForObject бросает EmptyResultDataAccessException вместо нужного нам null.

    -
    -

    Apply 3_10_db_implementation.patch

    - -
    -

    Проверьте, что в контекст Spring проекта включены оба файла конфигурации

    -
    -

    image

    -

    video 9. Тестирование UserService через AssertJ.

    -

    Apply 3_11_test_UserService.patch

    - -

    video 10. Логирование тестов.

    -

    Apply 3_12_test_logging.patch

    -
    - -
    - -

    Apply 3_13_fix_servlet.patch

    -

    Приложение перестало работать, тк. для репозитория мы используем заглушку JdbcMealRepositoryImpl

    -

    video 11. Ответы на Ваши вопросы

    -
      -
    • Что такое REST? 10 Best Practices for Better RESTful API
    • -
    • Зачем нужна сортировка еды?
    • -
    • Можно ли обойтись без MapSqlParameterSource?
    • -
    • Как происходит работа с DB в тестах?
    • -
    • Как реализовывать RowMapper?
    • -
    • Мои комментарии: решения проблем разработчиком.
    • -
    • Нужен ли разработчику JavaScript?
    • -
    -

    question Ваши вопросы

    -
    -

    Какая разница между @BeforeClass and @Before?

    -
    -

    @BeforeClass выполняется один раз после загрузки класса (поэтому метод может быть только статический), @Before перед каждым тестом. -Также: для чистоты тестов экземпляр тестового класса пересоздается перед каждым тестом: http://stackoverflow.com/questions/6094081/junit-using-constructor-instead-of-before

    -
    -

    Тесты в классе в каком-то определенном порядке выполняются ("сверху вниз" например)?

    -
    -

    Порядок по умолчанию неопределен, каждый тест должен быть автономен и не зависеть от других. См. также http://stackoverflow.com/questions/3693626/how-to-run-test-methods-in-specific-order-in-junit4

    -
    -

    Обязательно ли разворачивать postgreSQL?

    -
    -

    Желательно: хорошая и надежная ДБ:) Если совсем не хочется - можно работать со своей любимой RDBMS (поправить initDB.sql) или работать c postgresql в heroku (креденшелы к нему есть сверху в postgres.properties). На следующем уроке добавим HSQLDB, она не требует установки.

    -
    -

    Зачем начали индексацию с 100000?

    -
    -

    Тут нет "как принято". Так удобно вставлять в базу (если будет потребность) записи вручную не мешая счетчику.

    -
    -

    Из 5-го видео - "Логика в базе - большое зло". Можно чуть поподробней об этом?

    -
    -
      -
    • Есть успешные проекты с логикой в базе. Те все относительно.
    • -
    • Логика в базе - это процедуры и триггеры. Нет никакого ООП, переиспользовать код достаточно сложно, никагого рефакторинга, поиска по коду и других плюшек IDE. Нельзя делать всякие вещи типа кэширования, хранения в сесии - это все для логики на стороне java. Например json можно напрямую отдать в процедуру и там парсить и вставлять в таблицы или наоборот - собирать из таблиц и возвращать. -А затем потребуется некоторая логика на стороне приложения и все равно придется этот json дополнительно разпарсивать в java. -Я на таком проекте делал специальную миграцию, чтобы процедуры мигрировать не как sql скрипты, а каждую процедуру хранить как класс с историей изменений. Если логика: триггеры и простые процедуры записи-чтения, которые не требуют переиспользования кода или -проект небольшой это допустимо, иначе проект становится трудно поддерживать. Также иногда используют View для разграничения доступа. Например, для финансовых систем, таблицы проводок доступны только для админ учеток, а View просто не дадут увидеть (тем более изменить) данны обычному оператору на уровне СУБД.
    • -
    -
    -

    У JUnit есть ассерты и у спринга тоже. Можно ли обойтись без JUnit?

    -
    -

    Предусловия и JUnit-тесты совершенно разные вещи. Один другого не заменит, у нас будут предусловия в следующем уроке.

    -
    -

    Я так понял VARCHAR быстрее, чем TEXT, когда мы работаем с небольшими записями. Наши записи будут небольшими (255). Почему вы приняли решение перейти на TEXT?

    -
    -

    В отличие от MySql в Postgres VARCHAR и TEXT - тоже самое: http://stackoverflow.com/questions/4848964/postgresql-difference-between-text-and-varchar-character-varying

    -
    -

    Зачем при создании таблицы мы создаем CREATE UNIQUE INDEX и CREATE INDEX. При каких запросах он будет использоваться?

    -
    -

    UNIQUE индекс нужен для обеcпечения уникальности, DB не даст сделать одинаковый индекс. Индексы используется для скорости выполнения запросов. Обычно они задействуются, когда в запросе есть условия, на которые сделан индекс. Узнать по конкретному запросу можно запросив план запроса: см. Оптимизация запросов. Основы EXPLAIN в PostgreSQL. На измерение производительности с индексами посмотрим в следующем уроке.

    -
    -

    А это нормально, что у нас в базе у meals есть userId, а в классе - нет?

    -
    -

    Что значит - "нормально"? Приложение работает. Ненормально, когда в приложении есть "лишний" код, который не используется. Для ORM он нам понадобится- мы Meal.user добавим.

    -
    -

    Почему мы использует один sequence на разные таблицы?

    -
    -

    Мы будем использовать Hibernate, по умолчанию он делает глобальный sequence на все таблицы. В этом подходе есть как плюсы, так и минусы, из плюсов - удобно делать ссылки в коде и в таблицах на при наследовании и мапы в коде. В дополнение: Configure Hibernate to create separate sequence for each table by default.

    -

    hw Домашнее задание HW03

    -
      -
    • 1 Понять, почему перестали работать SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest
    • -
    • 2 Дополнить скрипты создания и инициализации базы таблицой MEALS. Запустить скрипты на вашу базу (через Run). Порядок таблиц при DROP и DELETE важен, если они связаны внешними ключами (foreign key, fk). Проверьте, что ваши скрипты работают
    • -
    • 3 Реализовать через Spring JDBC Template JdbcMealRepositoryImpl -
        -
      • 3.1. сделать каждый метод за один SQL запрос
      • -
      • 3.2. userId в класс Meal вставлять НЕ надо (для UI и REST это лишние данные, userId это id залогиненного пользователя)
      • -
      • 3.3. JbdcTemplate работает через сеттеры. Вместе с конструктором по умолчанию их нужно добавить в Meal
      • -
      • 3.4. Cписок еды должен быть отсортирован (тогда мы его сможем сравнивать с тестовыми данными). Кроме того это требуется для UI и API: последняя еда наверху.
      • -
      -
    • -
    • 4 Проверить работу MealServlet, запустив приложение
    • -
    -

    Optional

    -
      -
    • 5 Сделать MealServiceTest из MealService и реализовать тесты.
    • -
    -
    -

    По Ctrl+Shift+T (выбрать JUnit4) можно создать тест для конкретного класса, выбрав для него нужные методы. Тестовый класс создастся в папке test в том же пакете, что и тестируемый.

    -
    - -
    -

    question Как правильно придумать индекс для базы? Указать в нем все поля, комбинация которых создает по смыслу уникальную запись, или какие-то еще есть условия?

    -
    -

    Индекс нужно делать по тем полям, по которым будут искаться записи (участвуют в WHERE, ORDER BY). Уникальность - совсем не обязательное условие. Индексы ускоряют поиск по определенным полям таблицы. Они не бесплатные (хранятся в памяти, замедляется вставка), поэтому на всякий случай их делать не надо. Также не строят индексы на колонки с малым процентом уникальности (например поле "М/Ж"). Поля индекса НЕ КОММУТАТИВНЫ и порядок полей в описании индекса НЕОБХОДИМО соблюдать (в силу использования B-деревьев и их производных как поисковый механизм индекса). При построении плана запроса EXPLAIN учитывается количество записей в базе, поэтому вместо индексного поиска (Index Scan) база может выбрать последовательный (Seq Scan). Проверить, работают ли индексы можно отключив Seq Scan. Также см. Queries on the first field of composite index

    -

    error Решение проблем

    -
    -

    Из каталога main не видятся классы/ресурсы в test

    -
    -

    Все что находится в test используется только для тестов и недоступно в основном коде.

    -
    -

    Из IDEA не видятся ресурсы в каталоге test

    -
    -
      -
    • Сделайте Reimport All в Maven окне
    • -
    -

    image

    -
    -

    В UserServiceImpl и MealServiceImpl подчеркнуты красным repository, ошибка: Could not autowire. There is more than one bean of 'MealRepository' type.

    -
    -
      -
    • Spring test контекст не надо включать в Spring Facets проекта, там должны быть только spring-app.xml и spring-db.xml. Для тестовых контекстов поставьте чекбокс Check test files в Inspections.
    • -
    -

    image

    -

    error Проверка по HW03 (сначала сделайте самостоятельно!)

    -
      -
    • 1: В MealTestData еду делайте константами. Не надо Map конструкций!
    • -
    • 2: SQL case-insensitive, не надо писать в стиле Camel. В POSTGRES возможны case-sensitive значения, их надо в кавычки заключать (обычно не делают).
    • -
    • 3: ЕЩЕ РАЗ: InMemory тесты должны идти на InMemory репозитории
    • -
    • 4: Проверьте, что возвращает JdbcMealRepositoryImpl при обновлении чужой еды
    • -
    • 5: В реализации JdbcMealRepositoryImpl одним SQL запросом используйте возвращаемое update значение the number of rows affected
    • -
    • 6: При тестировании не портите констант из MealTestData
    • -
    • 7: Проверьте, что все, что относится к тестам, ноходится в каталоге test (не попадает в сборку проекта)
    • -
    • 8: Еще раз: в тестах проверять через JUnit Assert нельзя. Он проверяет через equals, который, учитавая будующие JPA реализации, мы не можем переопределить как сравнение по всем поля.
    • -
    • 9: НЕ делайте склейку SQL запросов вручную из параметров, только через jdbcTemplate параметры! См. Внедрение_SQL-кода
    • -
    • 10: Напомню: BeanPropertyRowMapper работает через отражение. Ему нужны геттеры/сеттеры и имена полей должны "совпадать" с колонками ResultSet (Column values are mapped based on matching the column name as obtained from result set metadata to public setters for the corresponding properties. The names are matched either directly or by transforming a name separating the parts with underscores to the same name using "camel" case).
    • -
    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Проверка по HW03 (сначала сделайте самостоятельно!) +- 1: В `MealTestData` еду делайте константами. Не надо `Map` конструкций! +- 2: SQL case-insensitive, не надо писать в стиле Camel. В POSTGRES возможны case-sensitive значения, их надо в кавычки заключать (обычно не делают). +- 3: ЕЩЕ РАЗ: `InMemory` тесты должны идти на `InMemory` репозитории +- 4: **Проверьте, что возвращает `JdbcMealRepositoryImpl` при обновлении чужой еды** +- 5: В реализации `JdbcMealRepositoryImpl` одним SQL запросом используйте возвращаемое `update` значение `the number of rows affected` +- 6: При тестировании не портите констант из `MealTestData` +- 7: Проверьте, что все, что относится к тестам, ноходится в каталоге `test` (не попадает в сборку проекта) +- 8: Еще раз: в тестах проверять через `JUnit Assert` нельзя. Он проверяет через `equals`, который, учитавая будующие JPA реализации, мы не можем переопределить как сравнение по всем поля. +- 9: НЕ делайте склейку SQL запросов вручную из параметров, только через `jdbcTemplate` параметры! См. [Внедрение_SQL-кода](https://ru.wikipedia.org/wiki/Внедрение_SQL-кода) +- 10: Напомню: `BeanPropertyRowMapper` работает через отражение. Ему нужны геттеры/сеттеры и имена полей должны "совпадать" с колонками `ResultSet` (Column values are mapped based on matching the column name as obtained from result set metadata to public setters for the corresponding properties. The names are matched either directly or by transforming a name separating the parts with underscores to the same name using "camel" case). \ No newline at end of file diff --git a/doc/lesson04.md b/doc/lesson04.md index ecd381df5800..28cc1a287bee 100644 --- a/doc/lesson04.md +++ b/doc/lesson04.md @@ -1,1024 +1,215 @@ +# Онлайн проект Topjava + +## Материалы занятия + +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правки в проекте +#### **Apply 4_0_fix.patch** +- В `JdbcUserRepositoryImpl.save()` и `UserServiceImpl.update` добавил проверку на несуществующей в базе `User.id` +- Зарефакторил в `InMemoryMealRepositoryImpl.getAll/getBetween`: `filter()` перед `sorted()` + +**Примечание**: в ответе на [Why is SELECT * considered harmful?](https://stackoverflow.com/questions/3639861) есть случаи, когда она допустима (наш случай): +> When * means "a row" + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW3 + +> `SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest` починим в патче **4_5_create_mock_test_ctx.patch (видео 4)** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. JdbcMealRepositoryImpl + MealServiceTest +#### **Apply 4_1_HW3.patch** +> - В `JdbcUserRepositoryImpl` поменял `MapSqlParameterSource` на `BeanPropertySqlParameterSource` (поля для вставки определяются через отражение в бине и метаданные в SQL запросе). + В `JdbcMealRepositoryImpl` остается `MapSqlParameterSource`, т.к. в отсутствует `Meal.userId`. См. дополнительно [CombinedSqlParameterSource](https://www.codota.com/java/spring/scenarios/549bbb5dda0a9536b85ad5f3/org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource?tag=spring1b) +> - Новый Postgres драйвер поддерживает Java 8 Date and Time. Преобразования c `Timestamp` уже не нужны. +> - В meals добавил составной индекс `INDEX meals_unique_user_datetime_idx ON meals(user_id, date_time)` для повышения скорости запросов по этим полям: +> - Удалил лишние `MealsUtil.MEALS` и `main()` + +- POSTGRESQL: BETWEEN CONDITION +- **[Сравнение времени выполнения для разных индексов](meals_index.md)** + - На id как на primary key индекс создается автоматически. + - все запросы в таблицу meals у нас идут с `user_id` + - по полю `date_time` также есть запросы + мы по нему сортируем список результатов, те они- хорошие кандидаты для индексирования. + - следует иметь в виду, индексы ускоряют операции чтения, но замедляют вставку и удаление, поэтому необходим анализ в реальном приложении + - [Оптимизация запросов. Основы EXPLAIN в PostgreSQL](https://habrahabr.ru/post/203320/) + - [Оптимизация запросов. Часть 2](https://habrahabr.ru/post/203386/) + - [Оптимизация запросов. Часть 3](https://habrahabr.ru/post/203484/) + - [Документация Postgres: индексы](https://postgrespro.ru/docs/postgresql/9.6/indexes.html) + +#### **Apply 4_2_HW3_optional.patch** + +## Занятие 4: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. Методы улучшения качества кода +- **Внимание!! В IDEA 2017.1 и 2 любой QAPlugin крэшится**. Можно заменить [FindBugs-IDEA](http://andrepdo.github.io/findbugs-idea) +- Сделайте интеграцию своего GitHub репозитория и подключите сверху своего [README.md](https://github.com/JavaWebinar/topjava/blob/master/README.md) ([Raw](https://raw.githubusercontent.com/JavaWebinar/topjava/master/README.md)) интергацию с + - Codacy Check code (проверка стиля и поиск багов в коде). В Codacy Issues + - отключил в настройках (remove pattern) проверку assert в JUnit (проверяем через матчеры) и [`meta http-equiv="content-type"` в JSP](https://stackoverflow.com/a/45440410/548473) + - сделал remove pattern на Cross Site Scripting (XSS), будем делать защиту на последнем занятии + - [VersionEye](https://www.versioneye.com) (проверка зависимостей Maven на актуальность). Кликни на значке `dependencies` в versioneye своего проекта, чтобы взять ссылку для `read.me` + - Сборку и тесты Travis (результат выполнения тестов проекта) + - [Travis CI Tutorial](https://dzone.com/articles/travis-ci-tutorial-java-projects) + - Сборка Java проекта + +#### **Apply 4_3_improve_code.patch** +> - Перенес проверки предусловий `Assert` из `InMemory` репозиториев в сервисы +> - Добавил конфигурацию `.travis.yml` +> - По [рапорту VersionEye](https://www.versioneye.com/user/projects/5a14435c0fb24f2a6d408cde) обновил зависимости Maven (будем пробовать Spring 5!) + +- Контрактное программирование, Программирование по контракту +- Comparison Preconditions in Java +- IDEA [Analyze | Inspect Code](https://www.jetbrains.com/help/idea/2017.1/running-inspections.html) +- QAPlug vs FindBugs + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Spring: инициализация и популирование DB +#### **Apply 4_4_init_and_populate_db.patch** +- [Инициализация базы при старте приложения](https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#jdbc-initializing-datasource-xml) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Подмена контекста при тестировании +#### **Apply 4_5_create_mock_test_ctx.patch** + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. ORM. Hibernate. JPA. +Entity- класс (объект Java), который в ORM маппится в таблицу DB. + +> - ВНИМАНИЕ: патч меняет `postgres.properties`, в котором у вас возможно свои креденшелы к базе. +> - `hibernate-core` 5.2.x включает `hibernate-entitymanager` и `hibernate-java8`, Time API конверторы уже не нужны. +> - JPA support for Java 8 new date and time API +> - What's new in Hibernate 5? +> - JPA support for Java 8 new date and time API +> - [EL implementation provided by the container. In a Java SE you have to add an implementation as dependency to your POM file](http://hibernate.org/validator/documentation/getting-started/#unified-expression-language-el): добавил `javax.el` зависимость со `scope=provided` + +#### **Apply 4_6_add_jpa.patch** +> - Тесты и приложение ломаются. `MealServiceTest` починится после выполнения HW04 (`JpaMealRepositoryImpl`) +> - При [настройке JPA в IDEA](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C-jpa) НЕ скачивайте библиотеку javaee.jar (и любую другую). **Все зависимости в проект попадают только через Maven.** +> - Если вы используете Java 9, то возникают проблемы с `JAXBException` (пакет `java.xml.bind`). [См. решение](https://www.concretepage.com/forum/thread?qid=531) +- Дополнительно: + - ORM. + - JPA и Hibernate в вопросах и ответах + - JPA EntityManager: управляем сущностями + - [Field vs property access](http://stackoverflow.com/a/6084701/548473) + - Hibernate: введение и написания Hello world приложения + - [15 reasons why we need to choose Hibernate over JDBC](https://habiletechnologies.com/blog/reasons-to-choose-hibernate-over-jdbc) + - Mapping: описания модели Hibernate (hbm.xml/annotation). + - Hibernate. Другие ORM: TopLink, EсlipseLink, EBean (used in Playframework). + - JPA (wiki). JPA (english wiki). JPA Performance Benchmark + - Отображения наследования объектов на таблицы + - Стратегии генерации PK + - hibernate-validator. JSR-303 -> JSR-349 + - Описание связей в модели. Ленивая загрузка объекта. + - JPA definitions + - Spring expressions: выражения в конфигурации + - HQL/ JPQL. + - Динамические запросы (которые формируются в коде): JPA Criteria API. Unified Queries for Java + - Using the Java 8 Date Time Classes with JPA + +#### **Apply 4_7_add_named_query_and_transaction.patch** + +- Транзакция. ACID. Уровни изоляции транзакций. +- Spring Transaction Management +- readOnly и Propagation.SUPPORTS +- `@Transactional` в тестах. Настройка EntityManagerFactory + +Справочник: + - Видео: Вячеслав Круглов — Как начинающему Java-разработчику подружиться со своей базой данных? + - Видео: Николай Алименков — Босиком по граблям Hibernate + - Стратегии работы с транзакциями + - Примеры работы с JPA + - Spring transaction propagation tutorial + - Getting Started with JPA + - Java Persistence + - Разделы по Java Persistence API + - Spring Framework transaction management + - Spring Persistence Tutorial + - Working with JPA Entity Objects + - Стратегии работы с транзакциями: Распространенные ошибки + - Принципы работы СУБД. MVCC + - MVCC + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Добавляем поддержку HSQLDB + +#### **Apply 4_8_add_hsqldb.patch** + +> - ВНИМАНИЕ: патч меняет `postgres.properties` +> - IDEA может `${jdbc.initLocation}` подчеркивать красным - тупит... + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> Есть несколько аналогичных "встроенных" баз данных. H2, HSQLDB, Derby, SQLite. Почему был выбран HSQLDB? + +Просто с ней приходилось работать. HSQLDB и H2 наиболее популярны, в новом курсе по spring-boot планирую использовать H2. +Здесь интересное краткое описание встраиваемых баз данных в Java. +В HSQLDB нет репликаций, кластеризайии и объем данным ограничен несколькими TB. Для большого количества приложений она подходит и для продакшена. См. +- What is HSQLDB limitations? +- HSQLDB в режиме in-process + +> Чистого JPA не существует, т.е. это всего лишь интерфейс, спецификация? Говорим JPA, подразумеваем какой-то ORM фрэймворк? А что тогда используют чистый jdbc, Spring-jdbc, MyBatis? MyBatis не реализует JPA? + +ORM это технология связывания БД и объектов приложения, а JPA - это JavaEE спецификация (API) этой технологии. +Реализации JPA - Hibernate, OpenJPA, EclipceLink, но, например, Hibernate может работать по собственному API (без JPA, которая появиласть позже). Spring-JDBC, MyBatis, JDBI не реализуют JPA, это обертки к JDBC. Все ORM и JPA также реализованы поверх JDBC. + +> В зависимостях maven `hibernate-entitymanager` тянет за собой `jboss-logging`. Как будет происходить логгирование? + +How do you configure logging in Hibernate 4 to use SLF4J: в нашем проекте автоматически подхватывается `logback-classic`. + +> В чем преимущество Hibernate ? + +Hibernate (как любая ORM) реализует маппинг таблиц в объекты Java. Когда мы добавим роли к пользователю вы увидете, насколько код будет проще, чем в jdbc. Также см. 5 Reasons to Use JPA / Hibernate + +> Чем отличается `@Column(nullable = false)` от `@NotNull` и есть ли необходимость указывать обе аннотации ? + +`@Column(nullable = false)` это атрибуты колонки таблицы базы. `@NotNull` - это валидация, которая происходит в приложении перед вставкой в базу. Если колонка ненулевая, то `NOT NULL` объязательна. Валидация- опциональна. Также см. +@NotNull vs @Column(nullable = false) + +> почему мы в в бине `entityManagerFactory` не указали диалект базы данных? + +Он [автоматически определяется из `DataSource` драйвера](http://stackoverflow.com/a/39817822/548473) + +> В чем разница между `persist` и `merge` + +Подробный ответ со Stackovwrflow с объяснением разницы. Упрощенно: + - `merge`, в отличие от `persist`, если entity нет в текущей сессии, делает запрос в базу данных + - entity, переданный в `merge` не меняется. Нужно использовать возвращаемый результат + +-------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW4 + +- 1: Сделать из `Meal` Hibernate entity + - Hibernate Validator: @NotNull, @NotEmpty, @NotBlank + - Реализация ManyToOne +- 2: Имплементировать и протестировать `JpaMealRepositoryImpl` + +#### Optional + +- 3: Добавить в тесты `MealServiceTest` функциональность `@Rule`: + - 3.1: проверку Exception + - 3.2: вывод в лог времени выполнения каждого теста + - 3.3: вывод сводки в конце класса: имя теста - время выполнения +- JUnit @Rules +- замена ExpectedException +--------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW4 +- 1: Тк. JPQL работает с объектами мы не можем использовать userId для сохранения. Можно сделать например так: + User ref = em.getReference(User.class, userId); + meal.setUser(ref); + При этом от User нам нужет только id. Создается lazy прокси над id, которая обращается к базе при запросе любого поля. Т.е. у нас запроса в базу за юзером не будет- проверьте по логам Hibernate +**Внимание: проверять запросы Hibernate нужно через run. Если делаете debug и брекпойнт, то могут делаться лишние запросы к базе (дебаггер дергает `toString`)** + +- 2: В JPQL запросах можно писать: `m.user.id=:userId` +- 3: При реализации `JpaMealRepositoryImpl` предпочтительно не использовать `try-catch` в логике реализации. Но если очень хочется, то ловить только специфичекские эксепшены (пр. `NoResultException`), чтобы, например, при отсутствии коннекта к базе приложение отвечало адекватно. +- 4: Мы будем смотреть генерацию db скриптов из модели, для корректной генерации нужно в `Meal` добавить `uniqueConstraints` +- 5: Когда будете чинить `MealServiceTest`, помните, что тесты пишутся для приложения, а не наоборот. +- 6: При записи в базу через `namedQuery` валидация ентити не работает, только валидация в бд +-------------------------------- +Новая информация плохо оседает в голове, когда дается в виде патчей, поэтому, чтобы она стала "твоей" нужно еще раз проделать это самостоятельно. Домашнее задание на этом уроке небольшое, а полученных знаний уже достаточно, чтобы после его выполнения начинать делать выпускной проект, сделанный на нашем стеке. +## [Выпускной проект](graduation.md) +- Для проекта я взял реальное тестовое задание, поэтому жалоб не неясность формулировок принимать не буду- сделайте как поняли. Представьте, что это ваше тестовое задание на работу. +- Общение в канале Slack *#graduate_project* +- Ревью проекта входит в участие с проверкой домашних заданий (ревьюится один раз!). Отдать на ревью нужно до 15.02 (если идешь на проект "Многомодульный maven. Многопоточность. XML (JAXB/StAX). + Веб сервисы (JAX-RS/SOAP). Удаленное взаимодействие (JMS/AKKA) (Masterjava)", то срок до 31.05). +- По завершению ты сможешь занести этот проект в свое портфолио и резюме как собственный, без всяких оговорок. +- Обязательно проверяйся [по рекомендациям в конце выпускного](graduation.md#-Рекомендации) - - - - - - - - - - - - - - - - - - - - - - - - topjava/lesson04.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - 6e6b896 - - Jan 17, 2018 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    -
    - -
    - -
    - 216 lines (165 sloc) - - 25 KB -
    -
    - - -
    -

    Онлайн проект Topjava

    -

    Материалы занятия

    -

    error Правки в проекте

    -

    Apply 4_0_fix.patch

    -
      -
    • В JdbcUserRepositoryImpl.save() и UserServiceImpl.update добавил проверку на несуществующей в базе User.id
    • -
    • Зарефакторил в InMemoryMealRepositoryImpl.getAll/getBetween: filter() перед sorted()
    • -
    -

    Примечание: в ответе на Why is SELECT * considered harmful? есть случаи, когда она допустима (наш случай):

    -
    -

    When * means "a row"

    -
    -

    hw Разбор домашнего задания HW3

    -
    -

    SpringMain, InMemoryAdminRestControllerTest, InMemoryAdminRestControllerSpringTest починим в патче 4_5_create_mock_test_ctx.patch (видео 4)

    -
    -

    video 1. JdbcMealRepositoryImpl + MealServiceTest

    -

    Apply 4_1_HW3.patch

    -
    -
      -
    • В JdbcUserRepositoryImpl поменял MapSqlParameterSource на BeanPropertySqlParameterSource (поля для вставки определяются через отражение в бине и метаданные в SQL запросе). -В JdbcMealRepositoryImpl остается MapSqlParameterSource, т.к. в отсутствует Meal.userId. См. дополнительно CombinedSqlParameterSource
    • -
    • Новый Postgres драйвер поддерживает Java 8 Date and Time. Преобразования c Timestamp уже не нужны.
    • -
    • В meals добавил составной индекс INDEX meals_unique_user_datetime_idx ON meals(user_id, date_time) для повышения скорости запросов по этим полям:
    • -
    • Удалил лишние MealsUtil.MEALS и main()
    • -
    -
    - -

    Apply 4_2_HW3_optional.patch

    -

    Занятие 4:

    -

    video 2. Методы улучшения качества кода

    -
      -
    • Внимание!! В IDEA 2017.1 и 2 любой QAPlugin крэшится. Можно заменить FindBugs-IDEA
    • -
    • Сделайте интеграцию своего GitHub репозитория и подключите сверху своего README.md (Raw) интергацию с -
        -
      • Codacy Check code (проверка стиля и поиск багов в коде). В Codacy Issues -
          -
        • отключил в настройках (remove pattern) проверку assert в JUnit (проверяем через матчеры) и meta http-equiv="content-type" в JSP
        • -
        • сделал remove pattern на Cross Site Scripting (XSS), будем делать защиту на последнем занятии
        • -
        -
      • -
      • VersionEye (проверка зависимостей Maven на актуальность). Кликни на значке dependencies в versioneye своего проекта, чтобы взять ссылку для read.me
      • -
      • Сборку и тесты Travis (результат выполнения тестов проекта) - -
      • -
      -
    • -
    -

    Apply 4_3_improve_code.patch

    -
    -
      -
    • Перенес проверки предусловий Assert из InMemory репозиториев в сервисы
    • -
    • Добавил конфигурацию .travis.yml
    • -
    • По рапорту VersionEye обновил зависимости Maven (будем пробовать Spring 5!)
    • -
    -
    - -

    video 3. Spring: инициализация и популирование DB

    -

    Apply 4_4_init_and_populate_db.patch

    - -

    video 4. Подмена контекста при тестировании

    -

    Apply 4_5_create_mock_test_ctx.patch

    -

    video 5. ORM. Hibernate. JPA.

    -

    Entity- класс (объект Java), который в ORM маппится в таблицу DB.

    -
    - -
    -

    Apply 4_6_add_jpa.patch

    -
    -
      -
    • Тесты и приложение ломаются. MealServiceTest починится после выполнения HW04 (JpaMealRepositoryImpl)
    • -
    • При настройке JPA в IDEA НЕ скачивайте библиотеку javaee.jar (и любую другую). Все зависимости в проект попадают только через Maven.
    • -
    • Если вы используете Java 9, то возникают проблемы с JAXBException (пакет java.xml.bind). См. решение
    • -
    -
    - -

    Apply 4_7_add_named_query_and_transaction.patch

    - -

    Справочник:

    - -

    video 6. Добавляем поддержку HSQLDB

    -

    Apply 4_8_add_hsqldb.patch

    -
    -
      -
    • ВНИМАНИЕ: патч меняет postgres.properties
    • -
    • IDEA может ${jdbc.initLocation} подчеркивать красным - тупит...
    • -
    -
    -

    question Ваши вопросы

    -
    -

    Есть несколько аналогичных "встроенных" баз данных. H2, HSQLDB, Derby, SQLite. Почему был выбран HSQLDB?

    -
    -

    Просто с ней приходилось работать. HSQLDB и H2 наиболее популярны, в новом курсе по spring-boot планирую использовать H2. -Здесь интересное краткое описание встраиваемых баз данных в Java. -В HSQLDB нет репликаций, кластеризайии и объем данным ограничен несколькими TB. Для большого количества приложений она подходит и для продакшена. См.

    - -
    -

    Чистого JPA не существует, т.е. это всего лишь интерфейс, спецификация? Говорим JPA, подразумеваем какой-то ORM фрэймворк? А что тогда используют чистый jdbc, Spring-jdbc, MyBatis? MyBatis не реализует JPA?

    -
    -

    ORM это технология связывания БД и объектов приложения, а JPA - это JavaEE спецификация (API) этой технологии. -Реализации JPA - Hibernate, OpenJPA, EclipceLink, но, например, Hibernate может работать по собственному API (без JPA, которая появиласть позже). Spring-JDBC, MyBatis, JDBI не реализуют JPA, это обертки к JDBC. Все ORM и JPA также реализованы поверх JDBC.

    -
    -

    В зависимостях maven hibernate-entitymanager тянет за собой jboss-logging. Как будет происходить логгирование?

    -
    -

    How do you configure logging in Hibernate 4 to use SLF4J: в нашем проекте автоматически подхватывается logback-classic.

    -
    -

    В чем преимущество Hibernate ?

    -
    -

    Hibernate (как любая ORM) реализует маппинг таблиц в объекты Java. Когда мы добавим роли к пользователю вы увидете, насколько код будет проще, чем в jdbc. Также см. 5 Reasons to Use JPA / Hibernate

    -
    -

    Чем отличается @Column(nullable = false) от @NotNull и есть ли необходимость указывать обе аннотации ?

    -
    -

    @Column(nullable = false) это атрибуты колонки таблицы базы. @NotNull - это валидация, которая происходит в приложении перед вставкой в базу. Если колонка ненулевая, то NOT NULL объязательна. Валидация- опциональна. Также см. -@NotNull vs @Column(nullable = false)

    -
    -

    почему мы в в бине entityManagerFactory не указали диалект базы данных?

    -
    -

    Он автоматически определяется из DataSource драйвера

    -
    -

    В чем разница между persist и merge

    -
    -

    Подробный ответ со Stackovwrflow с объяснением разницы. Упрощенно:

    -
      -
    • merge, в отличие от persist, если entity нет в текущей сессии, делает запрос в базу данных
    • -
    • entity, переданный в merge не меняется. Нужно использовать возвращаемый результат
    • -
    -
    -

    hw Домашнее задание HW4

    - -

    Optional

    -
      -
    • 3: Добавить в тесты MealServiceTest функциональность @Rule: -
        -
      • 3.1: проверку Exception
      • -
      • 3.2: вывод в лог времени выполнения каждого теста
      • -
      • 3.3: вывод сводки в конце класса: имя теста - время выполнения
      • -
      -
    • -
    • JUnit @Rules
    • -
    • замена ExpectedException
    • -
    -
    -

    error Подсказки по HW4

    -
      -
    • -

      1: Тк. JPQL работает с объектами мы не можем использовать userId для сохранения. Можно сделать например так:

      -
       User ref = em.getReference(User.class, userId);
      - meal.setUser(ref);
      -
      -

      При этом от User нам нужет только id. Создается lazy прокси над id, которая обращается к базе при запросе любого поля. Т.е. у нас запроса в базу за юзером не будет- проверьте по логам Hibernate

      -
    • -
    -

    Внимание: проверять запросы Hibernate нужно через run. Если делаете debug и брекпойнт, то могут делаться лишние запросы к базе (дебаггер дергает toString)

    -
      -
    • 2: В JPQL запросах можно писать: m.user.id=:userId
    • -
    • 3: При реализации JpaMealRepositoryImpl предпочтительно не использовать try-catch в логике реализации. Но если очень хочется, то ловить только специфичекские эксепшены (пр. NoResultException), чтобы, например, при отсутствии коннекта к базе приложение отвечало адекватно.
    • -
    • 4: Мы будем смотреть генерацию db скриптов из модели, для корректной генерации нужно в Meal добавить uniqueConstraints
    • -
    • 5: Когда будете чинить MealServiceTest, помните, что тесты пишутся для приложения, а не наоборот.
    • -
    • 6: При записи в базу через namedQuery валидация ентити не работает, только валидация в бд
    • -
    -
    -

    Новая информация плохо оседает в голове, когда дается в виде патчей, поэтому, чтобы она стала "твоей" нужно еще раз проделать это самостоятельно. Домашнее задание на этом уроке небольшое, а полученных знаний уже достаточно, чтобы после его выполнения начинать делать выпускной проект, сделанный на нашем стеке.

    -

    Выпускной проект

    - -

    Успехов в выполнении!

    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +### Успехов в выполнении! \ No newline at end of file diff --git a/doc/lesson05.md b/doc/lesson05.md index fe15d0c2bd74..1d92c381d076 100644 --- a/doc/lesson05.md +++ b/doc/lesson05.md @@ -1,1006 +1,198 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - topjava/lesson05.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - 25ada2d - - Nov 30, 2017 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    - -
    - -
    - 198 lines (152 sloc) - - 25.4 KB -
    -
    - - -
    -

    Онлайн проект Topjava

    -

    Материалы занятия

    -

    hw Разбор домашнего задания HW4

    -

    video 1. Разбор вопросов

    - -
    -

    EntityManager это по сути прокси обертка над Hibernate Session, которая создается каждый раз при открытии транзакции.

    -
    -
      -
    • Дополнительно (ни разу не сталкивался): еще есть редкий случай ручного управления @PersistenceContext(type = PersistenceContextType.EXTENDED), когда он используется в нескольних транзакциях (long-running session or session-per-conversation). - -
    • -
    -

    video 2. HW4: JPA. @Rule

    -

    Apply 5_1_HW4.patch

    -
    -
      -
    • При сравнении еды тесты падают, тк Hibernate делает ленивую обертку к user и если происходит обращение к любому его полю кроме id вне транзакции бросается LazyInitializationException. -По логике приложения мы достаем еду у AuthorizedUser, поле user нам не нужно и мы не будем его отдавать наружу: в тестах исключаем user из сравнения.
    • -
    • В Meal добавил @SuppressWarnings("JpaQlInspection"). Other warnings
    • -
    • Поменял реализацию JpaMealRepositoryImpl.get() (вместо @NamedQuery), реализация стали проще
    • -
    -
    -

    Apply 5_2_fix_hibernate_issue.patch

    -
    - -
    -
    -

    question Почему над AbstractBaseEntity стоит @Access(AccessType.FIELD) ? Почему при запросе user.id нам не нужно вытаскивать его из базы?

    -
    -

    AccessType.FIELD делает доступ в AbstractBaseEntity и всех классах-наследниках по полям. При загрузке Meal Hibernate на основе поля meal.user_id делает ленивую прокcи к User, у которой нет ничего, кроме id. Из-за бага хибернайт обращаелся за юзером в базу, хотя ничего кроме его id нам не надо. @Access(value = AccessType.PROPERTY) позволял обойти баг и не делать лишний запрос в базу.

    -

    Apply 5_3_HW4_optional.patch

    -
    - -
    -

    Занятие 5:

    -

    Раскрасил лог (в Spring Boot по умолчанию он тоже colored)

    -

    Apply 5_4_HW4_optional.patch

    - -

    video 3. Транзакции

    - -

    video 4. Профили Maven и Spring

    -

    Apply 5_5_profiles_connection_pool.patch

    -
    -
      -
    • Галочка в XML профиле влияет только на отображение в IDEA и никак на выполнение кода.
    • -
    • tomcat-jdbc со scope=provided не работает в Tomcat 7.x, поставьте Tomcat 8.x
    • -
    • Profiles.ACTIVE_DB задает активный профиль базы (postgres/hsqldb)
    • -
    • Profiles.REPOSITORY_IMPLEMENTATION определяет реализацию репозитория при запуске приложения (для тестов задаются через @ActiveProfiles).
    • -
    -
    -
    -

    Для переключения на HSQLDB необходимо:

    -
      -
    • поменять в окне Maven Projects профиль (Profiles) на hsqldb и сделать Reimport All Maven Projects (1я кнопка)
    • -
    • поменять Profiles.ACTIVE_DB = HSQLDB
    • -
    • почистить проект mvn clean (фаза clean не выполняется автоматически, чтобы каждый раз не перекомпилировать весь проект)
    • -
    -
    -

    Для корректного отображения неактивного профиля в IDEA проверьте флаг Inactive profile highlighting и сделайте проекту clean

    -

    image

    -
    -

    Вопрос: почему после этого патча не поднимется Spring при запуске приложения в Tomcat? (будем чинить в ДЗ п.6)

    -
    - -

    Автоматический выбор профиля базы: ActiveProfilesResolver

    -

    Apply 5_6_profile_resolver.patch

    -
    -

    Сделал автоматический выбор профиля базы при запуске приложения (тестов) в зависимости от присутствия драйвера базы в classpath (Profiles.getActiveDbProfile())

    -
    -

    video 5. Пул коннектов

    - -

    video 6. Spring Data JPA

    -

    Apply 5_7_spring_data_jpa.patch

    -
    -
      -
    • Переименовал классы Proxy* на более адекватные Crud*
    • -
    • В spring-framework-bom мы уже задали версию Spring. Убрал из остальных зависимостей.
    • -
    • В spring-data-jpa 2.x поменялся интерфейс: T CrudRepository.findOne(ID id) -> Optional<T> CrudRepository findById(ID id) - -
    • -
    -
    - -

    question Вопросы:

    -
    -

    Когда нужно ставить аннотацию @Modifying? Почему она стоит только над delete, но не над save?

    -
    -

    @Modifying ставится на модифицирующие @Query. Реализацию save можно посмотреть в org.springframework.data.jpa.repository.support.SimpleJpaRepository#save(), там она есть.

    -
    -

    Зачем мы переопределяем @Override Meal save в CrudUserRepository. Без этого все работает.

    -
    -

    Можно не переопределять. Сделал только для явного указания всех используемых методов из наследуемого CrudRepository

    -

    video 7. Spring кэш

    -

    Apply 5_8_spring_cache.patch

    -
    - -
    - -

    question Ваши вопросы

    -
    -

    В spring-petclinic DataJpa реализована без дополнительных классов. В таком виде как у них, spring data смотрится, конечно, намного лаконичней других реализаций, но у нас получилось вдвое больше кода, чем с тем же jpa или jdbc. Плюс только пожалуй в том, что query находятся прямо в репозитории, а не где-то там в другом пакете. Так что получается, spring data лучше подходит для простейших crud без всяких "фишек"? или в чем его достоинство для больших и сложных проектов?

    -
    -

    Достоинство DATA-JPA по сравнению например с JPA: есть типизация, готовые реализации типовых методов CRUD а также paging, data-common. Мы можем переключить реализацию JPA, например, на mongoDb (PagingAndSortingRepository, от которого наследуется JpaRepository, находится в spring-data-common). -Соответственно его методы будут поддерживаться всеми реализациями spring-data-common, JPA одна из них и пр. Подробнее о них есть в видео Spring Data – новый взгляд на persistence. +# Онлайн проект Topjava + +## Материалы занятия + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW4 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. Разбор вопросов +- Validate by RegExp +- Working with JPA Entity Objects +- Java Persistence/Relationships +- Использование ThreadLocal переменных +- Merge vs Persist +- Видео: работа в ZK с OpenJPA (в чем Hibernate хуже) +- Паттерн- открытие транзакции в фильтре и почему это bad-practice +- Sequence Strategies +- SequenceGenerator/IdentityGenerator in PostgreSql + +> `EntityManager` это по сути прокси обертка над Hibernate Session, которая создается каждый раз при открытии транзакции. + +- Дополнительно (ни разу не сталкивался): еще есть редкий случай ручного управления `@PersistenceContext(type = PersistenceContextType.EXTENDED)`, когда он используется в нескольних транзакциях (long-running session or session-per-conversation). + - Spring and PersistenceContextType.EXTENDED + - Transaction-scoped vs Extended Persistence + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW4: JPA. @Rule +#### Apply 5_1_HW4.patch +> - При сравнении еды тесты падают, тк Hibernate делает ленивую обертку к `user` и если происходит обращение к любому его полю кроме id вне транзакции бросается `LazyInitializationException`. +По логике приложения мы достаем еду у `AuthorizedUser`, поле `user` нам не нужно и мы не будем его отдавать наружу: в тестах исключаем `user` из сравнения. +> - В `Meal` добавил `@SuppressWarnings("JpaQlInspection")`. Other warnings +> - Поменял реализацию `JpaMealRepositoryImpl.get()` (вместо `@NamedQuery`), реализация стали проще + +#### Apply 5_2_fix_hibernate_issue.patch +> - Из за [Hibernate bug with proxy initialization when using `AccessType.FIELD`](https://hibernate.atlassian.net/browse/HHH-3718) + в `JpaMealRepositoryImpl.get()` делался дополнительный запрос в базу для инициализации прокси `User` и мы делали хак: доступ к полю `AbstractBaseEntity.id` через `AccessType.PROPERTY`. + Но в нашей версии `5.2.11.Final` этого запроса нет! Пока баг не закрыли держим под наблюдением (ссылка на багу в коде). +> - Which is better, field or property access? +> - Поправил `equals()` с учетом Lazy проксирования +> - JPA hashCode()/equals() dilemma +> - Hibernate Proxy Pitfalls + +> ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Почему над `AbstractBaseEntity` стоит `@Access(AccessType.FIELD)` ? Почему при запросе `user.id` нам не нужно вытаскивать его из базы? + +`AccessType.FIELD` делает доступ в `AbstractBaseEntity` и всех классах-наследниках по полям. При загрузке `Meal` Hibernate на основе поля `meal.user_id` делает ленивую прокcи к `User`, у которой нет ничего, кроме id. Из-за бага хибернайт обращаелся за юзером в базу, хотя ничего кроме его id нам не надо. `@Access(value = AccessType.PROPERTY)` позволял обойти баг и не делать лишний запрос в базу. + +#### Apply 5_3_HW4_optional.patch + +> - Hibernate 5.2.x already include Java 8 date and time types (JSR-310) +> - Stopwatch +> - Добавил сводку "имя теста - время выполнения" в конце класса + +## Занятие 5: + +### Раскрасил лог (в Spring Boot по умолчанию он тоже colored) +#### Apply 5_4_log_colored.patch +- [Logback layouts coloring](https://logback.qos.ch/manual/layouts.html#coloring) +- Дополнительно: [use colored output only when logging to a real terminal](https://stackoverflow.com/questions/31046748) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Транзакции +- wiki Транзакция +- readOnly и Propagation.SUPPORTS +- Ресурсы: + - How does Spring @Transactional Really Work + - Стратегии работы с транзакциями: Распространенные ошибки + - Spring @Transactional - isolation, propagation + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Профили Maven и Spring +#### Apply 5_5_profiles_connection_pool.patch +> - **Галочка в XML профиле влияет только на отображение в IDEA и никак на выполнение кода.** +> - **`tomcat-jdbc` со `scope=provided` не работает в Tomcat 7.x, поставьте Tomcat 8.x** +> - `Profiles.ACTIVE_DB` задает активный профиль базы (postgres/hsqldb) +> - `Profiles.REPOSITORY_IMPLEMENTATION` определяет реализацию репозитория при запуске приложения (для тестов задаются через `@ActiveProfiles`). + +> Для переключения на HSQLDB необходимо: +> - поменять в окне Maven Projects профиль (Profiles) на `hsqldb` и сделать `Reimport All Maven Projects` (1я кнопка) +> - поменять `Profiles.ACTIVE_DB = HSQLDB` +> - почистить проект `mvn clean` (фаза `clean` не выполняется автоматически, чтобы каждый раз не перекомпилировать весь проект) + +Для корректного отображения неактивного профиля в IDEA проверьте флаг _Inactive profile highlighting_ и сделайте проекту clean + +![image](https://cloud.githubusercontent.com/assets/13649199/25120020/29935958-2425-11e7-8363-1ff027426f64.png) + +> Вопрос: почему после этого патча не поднимется Spring при запуске приложения в Tomcat? (будем чинить в ДЗ п.6) + +- Using Spring Profiles in XML Config +- Spring Profiles example + +### Автоматический выбор профиля базы: [`ActiveProfilesResolver`](http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver) +#### Apply 5_6_profile_resolver.patch +> Сделал автоматический выбор профиля базы при запуске приложения (тестов) в зависимости от присутствия драйвера базы в classpath (`Profiles.getActiveDbProfile()`) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Пул коннектов +- Выбор реализации пула коннектов: BoneCP, Commons Database Connection Pooling, HikariCP +- Самый быстрый пул соединений на java (читаем комменты) +- Tomcat pool + + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Spring Data JPA +#### Apply 5_7_spring_data_jpa.patch +> - Переименовал классы _Proxy*_ на более адекватные _Crud*_ +> - В `spring-framework-bom` мы уже задали версию Spring. Убрал из остальных зависимостей. +> - В spring-data-jpa 2.x поменялся интерфейс: `T CrudRepository.findOne(ID id)` -> `Optional CrudRepository findById(ID id)` +> - [Java Optional — Отец холиваров](http://sboychenko.ru/java-optional) +> - [Java 8 Optional In Depth](https://www.mkyong.com/java8/java-8-optional-in-depth/) + +- Spring Data JPA +- Замена AbstractDAO: JPA Repositories +- Разрешение зависимостей: Maven BOM [Bill Of Materials] Dependency +- Делегирование (в конце статьи) +- Getting started with Spring Data JPA +- Query methods +- Spring Data – новый взгляд на persistence (JeeConf) +- Евгений Борисов — Spring Data? Да, та! +- Ресурсы: + - Github repositories + - Spring Data JPA Tutorial + - Spring Data JPA with QueryDSL + - [SpEL support in Spring Data JPA @Query](https://spring.io/blog/2014/07/15/spel-support-in-spring-data-jpa-query-definitions) + +![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопросы: + +> Когда нужно ставить аннотацию `@Modifying`? Почему она стоит только над delete, но не над save? + +`@Modifying` ставится на модифицирующие `@Query`. Реализацию save можно посмотреть в `org.springframework.data.jpa.repository.support.SimpleJpaRepository#save()`, там она есть. + +> Зачем мы переопределяем `@Override Meal save` в `CrudUserRepository`. Без этого все работает. + +Можно не переопределять. Сделал только для явного указания всех используемых методов из наследуемого `CrudRepository` + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Spring кэш +#### Apply 5_8_spring_cache.patch +> - Сделал миграцию на [Ehcache 3.x, compatibile with javax.cache API (JSR-107)](http://www.ehcache.org/) +> - [Spring 4+ with Ehcache 3 – how to](https://imhoratiu.wordpress.com/2017/01/26/spring-4-with-ehcache-3-how-to/) +> - [Новая XML конфигурация](http://www.ehcache.org/documentation/3.4/xml.html) +> - [Supplement JSR-107’s configurations](http://www.ehcache.org/documentation/3.1/107.html#supplement-jsr-107-configurations) +> - В `UserServiceTest.setUp` вместо вызова метода `UserService.evictCache` сделал очистку програмно через `CacheManager` +> - [Evict Ehcache elements programmatically, using Spring](https://stackoverflow.com/questions/29557959/evict-ehcache-elements-programmatically-using-spring) + +- Кеширование в Spring Framework +- Дополнительно: + - Spring cache Abstraction + - Распределённая система кеша ehcache + - Починка JUnit: один кэш на JVM + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> В spring-petclinic `DataJpa` реализована без дополнительных классов. В таком виде как у них, spring data смотрится, конечно, намного лаконичней других реализаций, но у нас получилось вдвое больше кода, чем с тем же jpa или jdbc. Плюс только пожалуй в том, что query находятся прямо в репозитории, а не где-то там в другом пакете. Так что получается, spring data лучше подходит для простейших crud без всяких "фишек"? или в чем его достоинство для больших и сложных проектов? + +Достоинство DATA-JPA по сравнению например с JPA: есть типизация, готовые реализации типовых методов CRUD а также paging, data-common. Мы можем переключить реализацию JPA, например, на mongoDb (`PagingAndSortingRepository`, от которого наследуется `JpaRepository`, находится в `spring-data-common`). +Соответственно его методы будут поддерживаться всеми реализациями `spring-data-common`, JPA одна из них и пр. Подробнее о них есть в видео Spring Data – новый взгляд на persistence. Дополнительное проксирование в DATA-JPA - моя "фишка" для устранения минусов этого фреймворка: невозможность дебага, привязка к интерфейсу JpaRepository, перенос логики Repository в слой сервисов. -Для большого приложения выигрыш этого стоит. Для небольших (тестовых) приложений дополнительных классов можно не делать.

    -
    -

    Почему мы для InMemory не сделали отдельного профиля? Почему их не удалить вообще?

    -
    -

    Реализация InMemory является примером как в test делать моки с подменой контекста. Для них сделали отдельный mock.xml и запускаемый проект ничего не должен о них знать. У нас учебный проект, в котором 4 реализации репозиториев, в реальном такого не будет.

    -
    -

    А как делать транзакционность для реализации jdbc?

    -
    -

    Будем делать на следующем уроке

    -
    -

    hw video Домашнее задание HW05

    -
      -
    • 1: Имплементировать DataJpaMealRepositoryImpl
    • -
    • 2: Разделить реализации Repository по профилям Spring: jdbc, jpa, datajpa (общее в профилях можно объединять, например <beans profile="datajpa,jpa">). -
        -
      • 2.1: Профили выбора DB (postgres/hsqldb) и реализации репозитория (jdbc/datajpa/jpa) независимы друг от друга и при запуске задать приложения (тестов) нужно задать тот и другой.
      • -
      • 2.2: Для интеграции с IDEA не забудте выставить в spring-db.xml справа вверху в Change Profiles... профили, например datajpa, postgres
      • -
      • 2.3: Общие части для всех в spring-db.xml можно оставить как есть без профилей вверху файла (до первого <beans profile= ).
      • -
      -
    • -
    • 3: Сделать тесты всех реализаций (jdbc, jpa, datajpa) через наследование (без дублирования) -
        -
      • 3.1 сделать один базовый класс для MealServiceTest и UserServiceTest.
      • -
      -
    • -
    • 4: Запустить все тесты: mvn test (в IDEA Maven Lifecycle - test, кнопку skipTest отжать)
    • -
    -

    Optional

    -
      -
    • 5: Разделить JdbcMealRepositoryImpl для HSQLDB (она не умеет работать с Java8 Time API) и Postgres через @Profile (для Postgres оставить LocalDateTime). Цель задания - потренироваться с паттерном шаблонный метод и профилями Spring. Бины Spring мы разделяем (фильтруем) по разным профилям с помощью beans profile в xml конфигурации и @Profile (те мы конфигурируем, какие бины попадут в контекст Spring в зависимости от активных профилей приложения). -Абстрактные классы не создаются и в контекст не попадают. Профили, заданные в @Profile пересекаются с активными профилями приложения: если пересечение есть, то бин включается в контекст (cм. реализацию @Profile и в ней ProfileCondition, можно подебажить). Например при сконфигуренном @Profile({"postgres","jdbc"}) бин попадет в контекст, если в профилях запущенного приложения есть хотя бы один из них (например "jdbc"). После выполнения разделения можно предложить решение проще.
    • -
    • 6: Починить MealServlet и использовать в SpringMain реализацию DB: добавить профили. Попробуйте поднять Spring контекст без использования spring.profiles.active.
    • -
    • 7: Сделать и протестировать в сервисах методы (тесты и реализация только для DataJpa) -
        -
      • 7.1: достать по id пользователя вместе с его едой
      • -
      • 7.2: достать по id еду вместе с пользователем
      • -
      • 7.3: обращения к DB сделать в одной транзакции (можно сделать разные варианты). Java Persistence/OneToMany
      • -
      -
    • -
    -
    -

    error Подсказки по HW05

    -
      -
    • 1: Для того, чтобы не запускались родительские классы тестов нужно сделать их abstract
    • -
    • 2: В реализациях JdbcMealRepository код не должен дублироваться. Если вы возвращаете тип Object, посмотрите в сторону дженериков.
    • -
    • 3: В MealServlet/SpringMain в момент setActiveProfiles контекст спринга еще не должен быть инициализирован, иначе выставление профиля уже ничего не будет делать.
    • -
    • 4: Если у метода нет реализации, то стандартно бросается UnsupportedOperationException.
    • -
    • 5: Для уменьшения количества кода при реализации Optional (п. 7, только DataJpa) попробуйте сделать default метод в интерфейсе
    • -
    • 6: В Data-Jpa метод для ссылки на entity (аналог em.getReference) : T getOne(ID id)
    • -
    • 7: Проверьте, что в DataJpaMealRepositoryImpl все обращения к DB выполняются в одной транзакции
    • -
    • 8: Для достать по id пользователя вместе с его едой я в User добавил List<Meal> meals
    • -
    • 9: Проверьте, что все тесты запускаются из Maven (имена классов тестов удовлетворяют соглашению) и итоги тестов класса выводятся корректно (не копятся)
    • -
    • 10: @ActiveProfiles принимает в качестве параметра строку, либо массив строк. В тестах можно задавать несколько @ActiveProfiles в разных классах, они суммируются
    • -
    • 11: В релизации 7.1 учесть, что у юзера может отсутствовать еда
    • -
    • 12: Ordering a join fetched collection in JPA using JPQL/HQL
    • -
    • 13: <beans profile= в конфигурации контекста должны находиться после всех остальных объявлений.
    • -
    -
    -
    - -
    - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +Для большого приложения выигрыш этого стоит. Для небольших (тестовых) приложений дополнительных классов можно не делать. + +> Почему мы для InMemory не сделали отдельного профиля? Почему их не удалить вообще? + +Реализация InMemory является примером как в test делать моки с подменой контекста. Для них сделали отдельный `mock.xml` и запускаемый проект ничего не должен о них знать. У нас учебный проект, в котором 4 реализации репозиториев, в реальном такого не будет. + +> А как делать транзакционность для реализации jdbc? + +Будем делать на следующем уроке + +-------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) Домашнее задание HW05 + +- 1: Имплементировать `DataJpaMealRepositoryImpl` +- 2: Разделить реализации Repository по профилям Spring: `jdbc`, `jpa`, `datajpa` (общее в профилях можно объединять, например ``). + - 2.1: Профили выбора DB (`postgres/hsqldb`) и реализации репозитория (`jdbc/datajpa/jpa`) независимы друг от друга и при запуске задать приложения (тестов) нужно задать тот и другой. + - 2.2: Для интеграции с IDEA не забудте выставить в `spring-db.xml` справа вверху в `Change Profiles...` профили, например `datajpa, postgres` + - 2.3: Общие части для всех в `spring-db.xml` можно оставить как есть без профилей вверху файла (до первого `Java Persistence/OneToMany + +--------------------- +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW05 +- 1: Для того, чтобы не запускались родительские классы тестов нужно сделать их `abstract` +- 2: В реализациях `JdbcMealRepository` **код не должен дублироваться**. Если вы возвращаете тип `Object`, посмотрите в сторону дженериков. +- 3: В `MealServlet/SpringMain` в момент `setActiveProfiles` контекст спринга еще не должен быть инициализирован, иначе выставление профиля уже ничего не будет делать. +- 4: Если у метода нет реализации, то стандартно бросается `UnsupportedOperationException`. +- 5: Для уменьшения количества кода при реализации _Optional_ (п. 7, только `DataJpa`) попробуйте сделать `default` метод в интерфейсе +- 6: В Data-Jpa метод для ссылки на entity (аналог `em.getReference`) : `T getOne(ID id)` +- 7: Проверьте, что в `DataJpaMealRepositoryImpl` все обращения к DB выполняются в **одной транзакции** +- 8: Для `достать по id пользователя вместе с его едой` я в `User` добавил `List meals` +- 9: Проверьте, что все тесты запускаются из Maven (имена классов тестов удовлетворяют соглашению) и итоги тестов класса выводятся корректно (не копятся) +- 10: `@ActiveProfiles` принимает в качестве параметра строку, либо **массив** строк. В тестах можно задавать несколько `@ActiveProfiles` в разных классах, они суммируются +- 11: В релизации 7.1 учесть, что у юзера может отсутствовать еда +- 12: [Ordering a join fetched collection in JPA using JPQL/HQL](http://stackoverflow.com/questions/5903774/ordering-a-join-fetched-collection-in-jpa-using-jpql-hql) +- 13: `Topjava +## Материалы занятия +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правки в проекте +#### Apply 6_0_fix.patch +> - Внес в `User` конструктор `registered` +> - Делаю копию `roles`, чтобы роли нельзя было изменить после инициализации. +> - В кэше `users` хранится единственный элемент - все юзеры +> - Мелкие правки +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW5 +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW5: Spring Profiles. Spring Data JPA +#### Apply 6_01_HW5_data_jpa.patch +> В `get` получаем и фильтруем `Optional` - - - - - - - - - - - +Транзакция начинается, когда встречается первый `@Transactional`. С default propagation `REQUIRED` остальные `@Transactional` просто участвуют в первой. Поэтому ставим ее сверху `DataJpaMealRepositoryImpl.save()`, чтобы все обращения к базе внутри метода были в одной транзакции. Анологично, если из сервиса собирается несколько запросов в репозитории, он ставится над методом сервиса. +#### Apply 6_02_HW5_profile_test.patch +**Для IDEA в `spring-db.xml` не забудте выставить Spring Profiles: например `datajpa, postgres`** - - - - - - +> - `DbTest` переименован в `AbstractServiceTest` и сюда перенес `@ActiveProfiles(resolver = ActiveDbProfileResolver.class)` +> - Заменил `description.getMethodName()` на `getDisplayName()` в выводе результатов тестов. После `printResult()` буфер сбрасывается в 0, чтобы не накапливать изменения. - - - topjava/lesson06.md at doc · JavaWebinar/topjava - - - +#### Apply 6_03_extract_rules.patch +> Вынес измерение времени и сводку в отдельный класс `TimingRules` - - +[JUnit Rules External Resources](https://carlosbecker.com/posts/junit-rules/#external-resources) - - - - - - +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW5: Optional - +#### Apply 6_04_HW5_optional_fix_jdbc_profiles.patch +> Сделал классы `Java8JdbcMealRepositoryImpl` и `TimestampJdbcMealRepositoryImpl` внутренними - - - - +- Spring Profiles. Spring 4 Conditional. +- зайдите в исходники `@Profile` и посмотрите (подебажте) его реализацию через `@Conditional(ProfileCondition.class)`. +- дополнительно: [реализация через Java Config и Profiles на уровне методов](http://stackoverflow.com/a/43645463/548473) - - +#### Apply 6_05_update_hsqldb.patch +В реальном проекте часто проблему можно решить простым обновлением версии: new HSQLDB version supports Java 8 time API +#### Apply 6_06_HW5_optional_fetch_join.patch +> - Добавил проверки и тесты на `NotFound` для `MealService.getWithUser` и `UserService.getWithMeals` +> - Убрал `CascadeType.REMOVE`, в уроке далее будет про Cascade. +- JPA JoinColumn vs mappedBy +- Unidirectional OneToMany +#### Apply 6_07_HW5_graph_batch_size.patch +> Сделал `@EntityGraph` [через `attributePaths`](https://spring.io/blog/2015/09/04/what-s-new-in-spring-data-release-gosling) - +- **N+1 selects issue** +- Using Named Entity Graphs + - [JPA ENTITY GRAPHS](http://www.radcortez.com/jpa-entity-graphs/) + - [`EntityGraphType.FETCH` vs `LOAD`](http://stackoverflow.com/questions/31978011/what-is-the-diffenece-between-fetch-and-load-for-entity-graph-of-jpa) +- Стратегии загрузки коллекций в JPA +- Стратегии загрузки коллекций в Hibernate +> Когда мы достаем всех юзеров с ролями без `@BatchSize` делается запрос юзеров (1) и на каждого юзера идет в базу запрос ролей (+N). +C `@BatchSize(size = 200)` делается запрос на юзеров (1) и затем роли достаются пачками для 200 юзеров (+ N/200). - +## Занятие 6: - - +### Добавил тесты на валидацию +> - к сожалению в JUnit нет `ExpectedException.expectRootCause`, а `expectCause` нам не подходит. В 13 JUnit обещают `expectThrows()`, а пока сделал вручную: +`AbstractServiceTest.validateRootCause()` - - + > ![](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Откуда у нас берется ConstraintViolationException в тестах на валидацию? Для каких наших исключений он является рутом? + + Прежде всего - пользуйтесь дебагом! Исключение легко увидеть в методе `getRootCause()`. Если подебажить выполение Hibernate валидации, то можно найти, где обрабатываются аннотации валидации и место в `org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate()`, где бросается `ConstraintViolationException`. - +#### Apply 6_08_add_test_validation.patch +**Тесты валидации для Jdbc не работают, нужно будет починить в HW6 (в реализация Jdbc валидация отсутствует)** - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Кэш Hibernate +> Кэш мигрировал на 3.x - - +#### Apply 6_09_hibernate_cache.patch +**Теперь уже все Jdbc тесты поломались. Требуется починить в HW6** - +- Уровни кэширования Hibernate +- Hibernate Cache. Практика +- Hibernate - Caching +- Починка тестов: инвалидация кэша Hibernate +- [Hibernate User Guide: Caching](http://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#caching) +- [Hibernate 5, Ehcache 3.x](http://www.boraji.com/hibernate-5-query-cache-entity-cache-and-collection-cache-example) +- Ресурсы: + - **Hibernate performance tuning (Mikalai Alimenkou /Igor Dmitriev)** + - JPA2 @Cacheable vs Hibernate @Cache + - How does Hibernate Query Cache work + - Pitfalls of the Hibernate Second-Level / Query Caches - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Cascade. Auto generate DDL. +#### Apply 6_10_cascade_ddl.patch +#### Cascading +> Есть SQL ON .. CASCADE, которая выполняется в базе данных и есть аннотация в Hibernate, исполняемая в приложении - +- Do not use `CascadeType` for @ManyToOne +- CascadeType meaning +- No cascade option on an ElementCollection, the target objects are always persisted, merged, removed with their parent. +- Create ON DELETE CASCADE: `@OnDelete` +- Hibernate second level cache and ON DELETE CASCADE in database schema +- [`orphanRemoval=true` vs `CascadeType.REMOVE`](http://stackoverflow.com/a/19645397/548473) +- [JPA `cascade/orphanRemoval` doesn't work with `NamedQuery`](http://stackoverflow.com/questions/7825484/jpa-delete-where-does-not-delete-children-and-throws-an-exception) +#### Auto schema generation +- JPA DATABASE SCHEMA GENERATION +- hbm2ddl.auto and autoincrement +- Hibernate/JPA DB Schema Generation Best Practices - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Spring Web +#### Apply 6_11_spring_web.patch +> - Для сборки проекта в окне Maven отключите тесты (`Toggele 'Skip Tests' Mode`) +> - В `web.xml` задаются профили запуска по умолчанию: `postgres,datajpa`. Если запускаетесь под HSQLDB, надо поменять на `hsqldb,datajpa`. +- Добавляем в проект веб зависимости +- Поднятие контекста Spring в веб приложении. ServletContextListener. Задание дефолтного профиля. +- Получение контекста Spring в веб-контейнере +- Servlet Lifecycle - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. JPS, JSTL, internationalization +#### Apply 6_12_jsp_jstl_i18n.patch +> Поменял `users\meals` в ключах локализации на `user\meal`. Понадобится при локализации ошибок (сделаем позже) +- Including Content in a JSP Page - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Динамическое изменение профиля при запуске. +> -Dspring.profiles.active="datajpa,postgres" - - +- Set profiles in Spring 3.1 - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Конфигурирование Tomcat через maven plugin. Jndi-lookup. +С плагином мы можем сконфигурировать Tomcat приямо в `pom.xml` и запустить его с задеплоенным туда нашим приложением WAR из командной строки +без IDEA и без инсталляции Tomcat. По умолчанию он скачивает его из центрального maven репозитория (можно также указать свой в `${container.home}`). +При запуске Tomcat из IDEA запускается Tomcat, путь к которому мы прописали в конфигурации запуска (со своими настройками). +#### Apply 6_13_tomcat_pool_jndi_cargo.patch +> - для запуска в Tomcat 8 поменял `tomcat7-maven-plugin` на `cargo-maven2-plugin`. +> - плагин сконфигурирован под postgres. Для HSQLDB нужно скорректировать `dependencies` и `driverClassName` в `context.xml` - +Запуск из коммандной строки: - + mvn clean package -DskipTests=true org.codehaus.cargo:cargo-maven2-plugin:1.6.5:run - - +- Cargo Maven2 plugin +- Катомизация context.xml в cargo-maven2-plugin +- Tomcat JNDI Resources +- BasicDataSource Configuration -
    - Skip to content -
    +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Spring Web MVC +#### Apply 6_14_spring_webmvc.patch +> - Починил путь к корню +> - В Spring 4.3 ввели новые аннотации `@Get/Post/...Mapping` (сокращенный вариант `@RequestMapping`) - - - +- Spring Web MVC +- Паттерн Front Controller +- Добавляем DispatcherServlet и MVC application context. +- Иерархия контекстов в Spring Web MVC +- Сценарий обработки запроса. HandlerMappings +- View Resolution: прячем jsp под WEB-INF. +- HandlerMapping: SimpleUrlHandlerMapping, BeanNameUrlHandlerMapping +- Маппинг ресурсов. +- Ресурсы: + - Spring MVC hello world + - Special bean types in the WebApplicationContext +> Настройки `Project Structure->Modules->Spring`: +![image](https://cloud.githubusercontent.com/assets/13649199/22221277/52c03cb4-e1c3-11e6-9039-08787e31a505.png) - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - bf0ccc5 - - Jan 8, 2018 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    - -
    - -
    - 306 lines (227 sloc) - - 34.4 KB -
    -
    - - -
    -

    Онлайн проекта Topjava

    -

    Материалы занятия

    -

    error Правки в проекте

    -

    Apply 6_0_fix.patch

    -
    -
      -
    • Внес в User конструктор registered
    • -
    • Делаю копию roles, чтобы роли нельзя было изменить после инициализации.
    • -
    • В кэше users хранится единственный элемент - все юзеры
    • -
    • Мелкие правки
    • -
    -
    -

    hw Разбор домашнего задания HW5

    -

    video 1. HW5: Spring Profiles. Spring Data JPA

    -

    Apply 6_01_HW5_data_jpa.patch

    -
    -

    В get получаем и фильтруем Optional<User>

    -
    -

    Транзакция начинается, когда встречается первый @Transactional. С default propagation REQUIRED остальные @Transactional просто участвуют в первой. Поэтому ставим ее сверху DataJpaMealRepositoryImpl.save(), чтобы все обращения к базе внутри метода были в одной транзакции. Анологично, если из сервиса собирается несколько запросов в репозитории, он ставится над методом сервиса.

    -

    Apply 6_02_HW5_profile_test.patch

    -

    Для IDEA в spring-db.xml не забудте выставить Spring Profiles: например datajpa, postgres

    -
    -
      -
    • DbTest переименован в AbstractServiceTest и сюда перенес @ActiveProfiles(resolver = ActiveDbProfileResolver.class)
    • -
    • Заменил description.getMethodName() на getDisplayName() в выводе результатов тестов. После printResult() буфер сбрасывается в 0, чтобы не накапливать изменения.
    • -
    -
    -

    Apply 6_03_extract_rules.patch

    -
    -

    Вынес измерение времени и сводку в отдельный класс TimingRules

    -
    -

    JUnit Rules External Resources

    -

    video 2. HW5: Optional

    -

    Apply 6_04_HW5_optional_fix_jdbc_profiles.patch

    -
    -

    Сделал классы Java8JdbcMealRepositoryImpl и TimestampJdbcMealRepositoryImpl внутренними

    -
    - -

    Apply 6_05_update_hsqldb.patch

    -

    В реальном проекте часто проблему можно решить простым обновлением версии: new HSQLDB version supports Java 8 time API

    -

    Apply 6_06_HW5_optional_fetch_join.patch

    -
    -
      -
    • Добавил проверки и тесты на NotFound для MealService.getWithUser и UserService.getWithMeals
    • -
    • Убрал CascadeType.REMOVE, в уроке далее будет про Cascade.
    • -
    -
    - -

    Apply 6_07_HW5_graph_batch_size.patch

    -
    -

    Сделал @EntityGraph через attributePaths

    -
    - -
    -

    Когда мы достаем всех юзеров с ролями без @BatchSize делается запрос юзеров (1) и на каждого юзера идет в базу запрос ролей (+N). -C @BatchSize(size = 200) делается запрос на юзеров (1) и затем роли достаются пачками для 200 юзеров (+ N/200).

    -
    -

    Занятие 6:

    -

    Добавил тесты на валидацию

    -
    -
      -
    • к сожалению в JUnit нет ExpectedException.expectRootCause, а expectCause нам не подходит. В 13 JUnit обещают expectThrows(), а пока сделал вручную: -AbstractServiceTest.validateRootCause()
    • -
    -
    -
    -

    Откуда у нас берется ConstraintViolationException в тестах на валидацию? Для каких наших исключений он является рутом?

    -
    -

    Прежде всего - пользуйтесь дебагом! Исключение легко увидеть в методе getRootCause(). Если подебажить выполение Hibernate валидации, то можно найти, где обрабатываются аннотации валидации и место в org.hibernate.cfg.beanvalidation.BeanValidationEventListener.validate(), где бросается ConstraintViolationException.

    -

    Apply 6_08_add_test_validation.patch

    -

    Тесты валидации для Jdbc не работают, нужно будет починить в HW6 (в реализация Jdbc валидация отсутствует)

    -

    video 3. Кэш Hibernate

    -
    -

    Кэш мигрировал на 3.x

    -
    -

    Apply 6_09_hibernate_cache.patch

    -

    Теперь уже все Jdbc тесты поломались. Требуется починить в HW6

    - -

    video 4. Cascade. Auto generate DDL.

    -

    Apply 6_10_cascade_ddl.patch

    -

    Cascading

    -
    -

    Есть SQL ON .. CASCADE, которая выполняется в базе данных и есть аннотация в Hibernate, исполняемая в приложении

    -
    - -

    Auto schema generation

    - -

    video 5. Spring Web

    -

    Apply 6_11_spring_web.patch

    -
    -
      -
    • Для сборки проекта в окне Maven отключите тесты (Toggele 'Skip Tests' Mode)
    • -
    • В web.xml задаются профили запуска по умолчанию: <param-value>postgres,datajpa</param-value>. Если запускаетесь под HSQLDB, надо поменять на hsqldb,datajpa.
    • -
    -
    -
      -
    • Добавляем в проект веб зависимости
    • -
    • Поднятие контекста Spring в веб приложении. ServletContextListener. Задание дефолтного профиля.
    • -
    • Получение контекста Spring в веб-контейнере
    • -
    • Servlet Lifecycle
    • -
    -

    video 6. JPS, JSTL, internationalization

    -

    Apply 6_12_jsp_jstl_i18n.patch

    -
    -

    Поменял users\meals в ключах локализации на user\meal. Понадобится при локализации ошибок (сделаем позже)

    -
    - -

    video 7. Динамическое изменение профиля при запуске.

    -
    -

    -Dspring.profiles.active="datajpa,postgres"

    -
    - -

    video 8. Конфигурирование Tomcat через maven plugin. Jndi-lookup.

    -

    С плагином мы можем сконфигурировать Tomcat приямо в pom.xml и запустить его с задеплоенным туда нашим приложением WAR из командной строки -без IDEA и без инсталляции Tomcat. По умолчанию он скачивает его из центрального maven репозитория (можно также указать свой в <container><home>${container.home}</home></container>). -При запуске Tomcat из IDEA запускается Tomcat, путь к которому мы прописали в конфигурации запуска (со своими настройками).

    -

    Apply 6_13_tomcat_pool_jndi_cargo.patch

    -
    -
      -
    • для запуска в Tomcat 8 поменял tomcat7-maven-plugin на cargo-maven2-plugin.
    • -
    • плагин сконфигурирован под postgres. Для HSQLDB нужно скорректировать dependencies и driverClassName в context.xml
    • -
    -
    -

    Запуск из коммандной строки:

    -
     mvn clean package -DskipTests=true org.codehaus.cargo:cargo-maven2-plugin:1.6.5:run
    -
    - -

    video 9. Spring Web MVC

    -

    Apply 6_14_spring_webmvc.patch

    -
    -
      -
    • Починил путь к корню
    • -
    • В Spring 4.3 ввели новые аннотации @Get/Post/...Mapping (сокращенный вариант @RequestMapping)
    • -
    -
    - -
    -

    Настройки Project Structure->Modules->Spring:

    -
    -

    image

    -
    -

    В web.xml мы инициализируем DispatcherServlet, передавая ему параметром spring-mvc.xml. Получается, что DispatcherServlet парсит spring-mvc.xml и находит в нем context?

    -
    -

    Нет. spring-mvc.xml парсит какой-то из спринговых ApplicationContext (думаю что XmlWebApplicationContext). -DispatcherServlet это сервлет, который раскидывает запросы по контроллерам (бинам контекста спринга). См паттерн Front Controller.

    -

    video 10. Spring Internationalization

    -

    Apply 6_15_spring_i18n.patch

    - -
    -
      -
    • В локализации поменял fmt:message на spring:message
    • -
    • Выбор языка зависит от языка операционной системы и хедера Accept-Language. Добавил в spring-mvc.xml messageSource параметр fallbackToSystemLocale. -Он управляет выбором, куда переключаться при выборе en и отсутствии app_en.properties: локаль операционной системы или app.properties (fallbackToSystemLocale=false). Переключение локалей будем реализовывать в конце проекта.
    • -
    -
    -

    Для тестирования локали можно поменять Accept-Language. Для хрома в chrome://settings/languages перетащить нужную локаль наверх.

    - -

    question Ваши вопросы

    -
    -

    Кэш hibernate надстраивается над ehcache или он живет самостоятельно?

    -
    -
      -
    • Understanding Hibernate Caching: -Hibernate supports following open-source cache implementations out-of-the-box: EHCache (Easy Hibernate Cache), OSCache (Open Symphony Cache), Swarm Cache, and JBoss Tree Cache.
    • -
    -
    -

    Где конфигурится интернализация для jstl (т.е. файл, где задаются app, app_ru.properties)? Достаточно указать в страницах бандл и путь в ресурсы?

    -
    -

    <fmt:setBundle basename="messages.app"/> означает что ресурсы будут искаться в classpath:messages/app(_xx)/properties: -Tag setBundle: fully-qualified resource name, which has the same form as a fully-qualified class name. -После сборки проекта maven их можно найти в target/classes или target/topjava/WEB-INF/classes.

    -
    -

    Отлично, что она все пишет на том языке, который пришел в хидере запроса. А если я хочу выбрать?

    -
    -

    Выбор языка зависит от языка операционной системы и хедера Accept-Language. Параметр fallbackToSystemLocale, который управляет выбором, когда с Accept-Language: en,en-US; не находится локализация app_en.properties. Для переключения локали используется JSTL Format Tag fmt:setLocale. Мы будем реализовывать переключение локалей в Spring i18n в конце проекта.

    -
    -

    Мы создаем бин, где получаем dataSource по имени <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/topjava"/>. -Но там не указан класс, как в других dataSource? Получается по имени jdbc/topjava нам уже отдает готовый обьект dataSource и мы как бы помещаем его в бин?

    -
    -

    Здесь используется namespace jee:jndi-lookup, который прячет под собой классы реализации. JNDI объект DataSource конфигурируется в src/main/resources/tomcat/context.xml

    -
    -

    В плагине прописан профиль <spring.profiles.active>tomcat,datajpa</spring.profiles.active>, а в web.xml <param-value>postgres,datajpa</param-value>. -Какой же реально отрабатывает?

    -
    -

    См. видео урока "Динамическое изменение профиля при запуске". В плагине мы задаем параметры JVM запуска Tomcat

    -
    -

    Почему мы не используем элемент <context:annotation-config/> в spring-db.xml?

    -
    -

    В проекте у нас сейчас 2 Spring контекста: spring-mvc.xml (см. web.xml, DispatcherServlet) и родительский spring-app.xml + spring-db.xml (web.xml, contextConfigLocation). -Грубо: 2 мапы, причем для mvc доступно все что есть в родителе. Те spring-db.xml не является отдельным самостоятельным контекстом и достаточно того, что <context:annotation-config/> у нас есть в spring-app.xml.

    -
    -

    A @NamedQuery или @Query подвержены кешу запросов? Т.е. если мы поставим USE_QUERY_CACHE_value="true" будет Hibernate их кешировать?

    -
    -

    Чтобы запрос кэшировался, кроме true в конфигурации нужно еще явно выставить запросу setCacheable (http://vladmihalcea.com/2015/06/08/how-does-hibernate-query-cache-work/). По поводу кэширования @NamedQuery нашел @QueryHint: https://docs.jboss.org/jbossas/docs/Clustering_Guide/5/html/ch04s02s03.html

    -
    -

    Почему messages мы кладем в config и используем system environment? разве так делают в реальном проекте? не будешь же вписывать на сервере эти переменные каждый раз, если проект куда-то будет переезжать. Можно по другому, кроме systemEnvironment['TOPJAVA_ROOT'] задать путь от корня проекта?

    -
    -
      -
    1. -

      messages нам нужны в runtime (при работе приложения). Проект к собранному и задеплоенному в Tomcat war отношения никакого уже не имеет и на этом сервере он обычно не находится. Если ресурсы нужны только при сборке и тестировании, то путь к корню для одномодульного maven проекта можно задать как ${project.basedir}, но для многомодульного проекта (а все реальные проекты многомодульные) это путь к корню своего модуля.

      -
    2. -
    3. -

      В "реальном приложении" делается совершенно по разному:

      -
        -
      • нести с собой в classpath, но ресурсы нельзя будет динамически (без передеплоя) обновлять
      • -
      • класть в war (не в classpath) и обновлять в развернутом TOMCAT_HOME/webapps/[appname]/...
      • -
      • класть в зафиксированное определенное место (например в home: ~ или в путь от корня /app/config). Можно задавать фиксированный пусть в пропертях профиля maven и фильтровать ресурсы (maven resources), чтобы они попали в проперти проекта.
      • -
      • делать через переменную окружения, как у нас
      • -
      • задавать в параметрах запуска JVM как системную переменную через -D..
      • -
      • располагать в преференсах (для unix это home, для windows- registry): использование Preferences API
      • -
      • держать настройки в DB
      • -
      -

      Часто в одном приложении используют несколько способов для разных видов конфигураций.

      -
    4. -
    -
    -

    Не происходит ли дублирования при кэшировании пользователей чрез Hibernate и @Cacheable ?

    -
    -

    @Cacheable кэширует результат запроса getAll(), те список юзеров. Hibernate кэширует юзеров по отдельности, те, грубо мапа, id->User. Те можно назвать это дублированием. Нужно ли будет такое в реальном приложении - все смотрится из логики запросов и их частоты, вполне вероятно что нет. Как то мы писали приложение для Дойчебанка (аналог skype на GWT, те на экране небольшое окошко)- там было 5!! уровней кэширования, первый вообще в базе.

    -
    -

    У меня стоит томкат 7 версии, в помнике у нас 8 прописан, но всё работает. Почему?

    -
    -

    В pom.xml мы подключаем servlet-api версии 3.1 со scope=provided, что означает что он используется только для компиляции и не идет в war. Тк мы не используем никаких фич 3.1, то наш код совместим с Tomcat 7.x. При запуске через cargo-maven2-plugin Tomcat 8 загружается из maven репозитория.

    -
    -

    Откуда @Transactional вытягивает класс для работы с транзакцией, в составе какого бина он идет?

    -
    -
      -
    1. Если в контексте Spring есть <tx:annotation-driven/>, то подключается BeanPostProcessors, который проксирует классы (и методы), помеченные @Transactional.
    2. -
    3. По умолчанию для TransactionManager используется бин с id=transactionManager
    4. -
    -
    -

    hw Домашнее задание HW06

    -
      -
    • 1.1 Починить тесты InMemoryAdminRestControllerSpringTest/InMemoryAdminRestControllerTest (в новой версии Spring классы spring-mvc требуют WebApplicationContext, поэтому поправьте mock.xml)
    • -
    • 1.2 Починить Jdbc тесты (валидацию исключить) - -
    • -
    • 1.3 Удалить сервлеты и перенести функциональность MealServlet в JspMealController контроллер (по аналогии с RootController). -MealRestController у нас останется, с ним будем работать позже. -
        -
      • 1.3.1 разнести запросы на update/delete/.. по разным методам (попробуйте вообще без action=). Можно по аналогии с RootController#setUser принимать HttpServletRequest request (аннотации на параметры и адаптеры для LocalDate/Time мы введем позже).
      • -
      • 1.3.2 в одном контроллере нельзя использовать другой. Чтобы не дублировать код можно сделать наследование.
      • -
      • 1.3.3 добавить локализацию и jsp:include в mealForm.jsp / meals.jsp
      • -
      -
    • -
    -

    Optional

    -
      -
    • 2.1 Добавить транзакционность (DataSourceTransactionManager) в Jdbc реализации
    • -
    • 2.2 Добавить еще одну роль к ADMIN (будет 2 роли: ROLE_USER, ROLE_ADMIN)
    • -
    • 2.3 Добавить проверку ролей в UserTestData.assertMatch
    • -
    • 2.4 Починить тесты в JdbcUserRepositoryImpl (добавить роли). Доставать можно двумя способами: одним запросом с JOIN либо двумя запросами: отдельно users и отдельно roles. -
        -
      • 2.4.1 В реализации getAll НЕ делать запрос ролей для каждого юзера (N+1 select)
      • -
      • 2.4.2 При save посмотрите на batchUpdate()
      • -
      -
    • -
    -
    -

    error Подсказки по HW06

    -
      -
    • 1: Неверная кодировка UTF-8 с Spring обычно решается фильтром CharacterEncodingFilter:
    • -
    -
        <filter>
    -        <filter-name>encodingFilter</filter-name>
    -        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    -        <init-param>
    -            <param-name>encoding</param-name>
    -            <param-value>UTF-8</param-value>
    -        </init-param>
    -        <init-param>
    -            <param-name>forceEncoding</param-name>
    -            <param-value>true</param-value>
    -        </init-param>
    -    </filter>
    -    <filter-mapping>
    -        <filter-name>encodingFilter</filter-name>
    -        <url-pattern>/*</url-pattern>
    -    </filter-mapping>
    -
    -
      -
    • 2: Если не поднимается контекст Spring, смотрим причину в верху самого нижнего эксепшена. Все ошибки на отсутствия бина в контексте или его нескольких реализациях относятся к пониманию основ: Spring application context. Если нет понимания этих основ, двигаться дальше нельзя, нужно вернуться к видео Спринг, где объясняется что это такое. Также пересмотрите видео Тестирование UserService через AssertJ. Начиная с 11.30 как раз разбираются подобные ошибки.
    • -
    • 3: Если неправильно формируется url относительно контекста приложения (например /topjava/meals/meals), посмотрите на - -
    • -
    • 4: При проблемах с запуском томкат проверьте запущенные java процессы, нет ли в TOMCAT_HOME\webapps приложения каталога topjava, логи tomcat - нет ли проблем с доступом к каталогам или контекстом Spring.
    • -
    • 5: Если создаете List с одним значением или Map с одним ключом-значением, пользуйтесь Collections.singleton..
    • -
    • 6: В MealController общую часть @RequestMapping(value = "/meals") лучше вынести на уровень класса
    • -
    • 7: Не забывайте при реализации JdbcUserRepositoryImpl про Map.computeIfAbsent и EnumSet
    • -
    • 8: Проверьте @Transactional(readOnly = true) сверху Jdbc..RepositoryImpl
    • -
    • 9: Проверьте, что config\messages\app_ru.properties у вас в кодировке UTF-8 (в любом редакторе/вьюере или при отключенном Transparent native-to-ascii conversion в IDEA). ASCII коды нужны были только для JSP.
    • -
    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - +- Reloadable MessageSources +- nginx: Serving Static Content +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Кэш hibernate надстраивается над ehcache или он живет самостоятельно? + +- Understanding Hibernate Caching: +Hibernate supports following open-source cache implementations out-of-the-box: EHCache (Easy Hibernate Cache), OSCache (Open Symphony Cache), Swarm Cache, and JBoss Tree Cache. + +> Где конфигурится интернализация для jstl (т.е. файл, где задаются app, app_ru.properties)? Достаточно указать в страницах бандл и путь в ресурсы? + +`` означает что ресурсы будут искаться в `classpath:messages/app(_xx)/properties`: +Tag setBundle: fully-qualified resource name, which has the same form as a fully-qualified class name. +После сборки проекта maven их можно найти в `target/classes` или `target/topjava/WEB-INF/classes`. + +> Отлично, что она все пишет на том языке, который пришел в хидере запроса. А если я хочу выбрать? + +Выбор языка зависит от языка операционной системы и хедера `Accept-Language`. Параметр `fallbackToSystemLocale`, который управляет выбором, когда с `Accept-Language: en,en-US;` не находится локализация `app_en.properties`. Для переключения локали используется JSTL Format Tag fmt:setLocale. Мы будем реализовывать переключение локалей в Spring i18n в конце проекта. + +> Мы создаем бин, где получаем dataSource по имени ``. +Но там не указан класс, как в других dataSource? Получается по имени jdbc/topjava нам уже отдает готовый обьект dataSource и мы как бы помещаем его в бин? + +Здесь используется namespace `jee:jndi-lookup`, который прячет под собой классы реализации. JNDI объект DataSource конфигурируется в `src/main/resources/tomcat/context.xml` + +> В плагине прописан профиль `tomcat,datajpa`, а в web.xml `postgres,datajpa`. +Какой же реально отрабатывает? + +См. видео урока "Динамическое изменение профиля при запуске". В плагине мы задаем параметры JVM запуска Tomcat + +> Почему мы не используем элемент `` в `spring-db.xml`? + +В проекте у нас сейчас 2 Spring контекста: `spring-mvc.xml (см. web.xml, DispatcherServlet)` и родительский `spring-app.xml + spring-db.xml (web.xml, contextConfigLocation)`. +Грубо: 2 мапы, причем для mvc доступно все что есть в родителе. Те `spring-db.xml` не является отдельным самостоятельным контекстом и достаточно того, что `` у нас есть в `spring-app.xml`. + +> A `@NamedQuery` или `@Query` подвержены кешу запросов? Т.е. если мы поставим _USE_QUERY_CACHE_value_="true" будет Hibernate их кешировать? + +Чтобы запрос кэшировался, кроме true в конфигурации нужно еще явно выставить запросу _setCacheable_ (http://vladmihalcea.com/2015/06/08/how-does-hibernate-query-cache-work/). По поводу кэширования `@NamedQuery` нашел `@QueryHint`: https://docs.jboss.org/jbossas/docs/Clustering_Guide/5/html/ch04s02s03.html + +> Почему messages мы кладем в config и используем system environment? разве так делают в реальном проекте? не будешь же вписывать на сервере эти переменные каждый раз, если проект куда-то будет переезжать. Можно по другому, кроме systemEnvironment['TOPJAVA_ROOT'] задать путь от корня проекта? + +1. messages нам нужны в runtime (при работе приложения). Проект к собранному и задеплоенному в Tomcat war отношения никакого уже не имеет и на этом сервере он обычно не находится. Если ресурсы нужны только при сборке и тестировании, то путь к корню для одномодульного maven проекта можно задать как `${project.basedir}`, но для многомодульного проекта (а все реальные проекты многомодульные) это путь к корню своего модуля. +2. В "реальном приложении" делается совершенно по разному: + - нести с собой в classpath, но ресурсы нельзя будет динамически (без передеплоя) обновлять + - класть в war (не в classpath) и обновлять в развернутом TOMCAT_HOME/webapps/[appname]/... + - класть в зафиксированное определенное место (например в home: `~` или в путь от корня `/app/config`). Можно задавать фиксированный пусть в пропертях профиля maven и фильтровать ресурсы (maven resources), чтобы они попали в проперти проекта. + - делать через переменную окружения, как у нас + - задавать в параметрах запуска JVM как системную переменную через -D.. + - располагать в преференсах (для unix это home, для windows- registry): использование Preferences API + - держать настройки в DB + + Часто в одном приложении используют несколько способов для разных видов конфигураций. + +> Не происходит ли дублирования при кэшировании пользователей чрез Hibernate и `@Cacheable` ? + +`@Cacheable` кэширует результат запроса `getAll()`, те список юзеров. Hibernate кэширует юзеров по отдельности, те, грубо мапа, id->User. Те можно назвать это дублированием. Нужно ли будет такое в реальном приложении - все смотрится из логики запросов и их частоты, вполне вероятно что нет. Как то мы писали приложение для Дойчебанка (аналог skype на GWT, те на экране небольшое окошко)- там было 5!! уровней кэширования, первый вообще в базе. + +> У меня стоит томкат 7 версии, в помнике у нас 8 прописан, но всё работает. Почему? + +В `pom.xml` мы подключаем servlet-api версии 3.1 со `scope=provided`, что означает что он используется только для компиляции и не идет в war. Тк мы не используем никаких фич 3.1, то наш код совместим с Tomcat 7.x. При запуске через `cargo-maven2-plugin` Tomcat 8 загружается из maven репозитория. + +> Откуда `@Transactional` вытягивает класс для работы с транзакцией, в составе какого бина он идет? + +1. Если в контексте Spring есть ``, то подключается `BeanPostProcessors`, который проксирует классы (и методы), помеченные `@Transactional`. +2. По умолчанию для TransactionManager используется бин с `id=transactionManager` + +--------------------------- + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW06 +- 1.1 Починить тесты `InMemoryAdminRestControllerSpringTest/InMemoryAdminRestControllerTest` (в новой версии Spring классы `spring-mvc` требуют `WebApplicationContext`, поэтому поправьте `mock.xml`) +- 1.2 Починить Jdbc тесты (валидацию исключить) + - org.junit.Assume + - How to get Current Profiles in Spring Application +- 1.3 Удалить сервлеты и перенести функциональность `MealServlet` в `JspMealController` контроллер (по аналогии с `RootController`). +`MealRestController` у нас останется, с ним будем работать позже. + - 1.3.1 разнести запросы на update/delete/.. по разным методам (попробуйте вообще без `action=`). Можно по аналогии с `RootController#setUser` принимать `HttpServletRequest request` (аннотации на параметры и адаптеры для `LocalDate/Time` мы введем позже). + - 1.3.2 в одном контроллере нельзя использовать другой. Чтобы не дублировать код можно сделать наследование. + - 1.3.3 добавить локализацию и `jsp:include` в `mealForm.jsp / meals.jsp` + +#### Optional +- 2.1 Добавить транзакционность (`DataSourceTransactionManager`) в Jdbc реализации +- 2.2 Добавить еще одну роль к ADMIN (будет 2 роли: `ROLE_USER, ROLE_ADMIN`) +- 2.3 Добавить проверку ролей в UserTestData.assertMatch +- 2.4 Починить тесты в `JdbcUserRepositoryImpl` (добавить роли). Доставать можно двумя способами: одним запросом с JOIN либо двумя запросами: отдельно `users` и отдельно `roles`. + - 2.4.1 В реализации `getAll` НЕ делать запрос ролей для каждого юзера (N+1 select) + - 2.4.2 При save посмотрите на batchUpdate() + +--------------------- +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW06 +- 1: Неверная кодировка UTF-8 с Spring обычно решается фильтром `CharacterEncodingFilter`: +``` + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + +``` +- 2: **Если не поднимается контекст Spring, смотрим причину в верху самого нижнего эксепшена.** Все ошибки на отсутствия бина в контексте или его нескольких реализациях относятся к пониманию основ: Spring application context. Если нет понимания этих основ, двигаться дальше нельзя, нужно вернуться к видео Спринг, где объясняется что это такое. Также пересмотрите видео [Тестирование UserService через AssertJ](https://drive.google.com/file/d/1SPMkWMYPvpk9i0TA7ioa-9Sn1EGBtClD). Начиная с 11.30 как раз разбираются подобные ошибки. +- 3: Если неправильно формируется url относительно контекста приложения (например `/topjava/meals/meals`), посмотрите на + - Relative paths in JSP + - Spring redirect: prefix +- 4: При проблемах с запуском томкат проверьте запущенные `java` процессы, нет ли в `TOMCAT_HOME\webapps` приложения каталога `topjava`, логи tomcat - нет ли проблем с доступом к каталогам или контекстом Spring. +- 5: Если создаете List с одним значением или Map с одним ключом-значением, пользуйтесь `Collections.singleton..` +- 6: В MealController общую часть `@RequestMapping(value = "/meals")` лучше вынести на уровень класса +- 7: Не забывайте при реализации `JdbcUserRepositoryImpl` про `Map.computeIfAbsent` и `EnumSet` +- 8: Проверьте `@Transactional(readOnly = true)` сверху `Jdbc..RepositoryImpl` +- 9: Проверьте, что `config\messages\app_ru.properties` у вас в кодировке UTF-8 (в любом редакторе/вьюере или при отключенном [Transparent native-to-ascii conversion](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%9F%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C-%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D1%83-utf-8) в IDEA). ASCII коды нужны были только для JSP. \ No newline at end of file diff --git a/doc/lesson07.md b/doc/lesson07.md index 6ccc8b6d50c8..1bd05c90c2d1 100644 --- a/doc/lesson07.md +++ b/doc/lesson07.md @@ -1,936 +1,154 @@ +# Онлайн проекта Topjava + +## Материалы занятия + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW6 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW6 +#### Apply 7_01_HW6_fix_tests.patch +> - Добавил `AbstractServiceTest.isJpaBased()` и `Assume.assumeTrue(isJpaBased())` в `AbstractMealServiceTest.testValidation()`. +> - Как вариант можно было вместо наследования от `AbstractJpaUserServiceTest` сделать `@Autowired(required = false) JpaUtil` (в этом случае, если в профиле jpa/datajpa не объявить `JpaUtil`, тестах будет NPE) +> - В новой версии Spring классы `spring-mvc` требуют `WebApplicationContext`, поэтому поправил `mock.xml` + + +#### Apply 7_02_HW6_meals.patch +При переходе на AJAX `JspMealController` удалим за ненадобностью, возвращение всей еды `meals()` останется в `RootController`. + +#### Apply 7_03_HW6_fix_relative_url_utf8.patch +- Relative paths in JSP +- Spring redirect: prefix + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW6 Optional +#### Apply 7_04_HW6_optional_add_role.patch +#### Apply 7_05_fix_hint_graph.patch +- В `JpaUserRepositoryImpl.getByEmail` DISTINCT попадает в запрос, хотя он там не нужен. Это просто указание Hibernate не дублировать данные. +Для оптимизации можно указать Hibernate делать запрос без distinct: [15.16.2. Using DISTINCT with entity queries](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct) +- Тест `DataJpaUserServiceTest.testGetWithMeals()` не работает для admin (у админа 2 роли и еда при JOIN дублируется). `DISTINCT` при несколький JOIN не помогает. +Оставил в графе только `meals`. Корректно поставить тип `LOAD`, чтобы остальные ассоциации доставались по стратегии модели. Однако [с типом по умолчанию `FETCH` роли также достаются](https://stackoverflow.com/a/46013654/548473) (похоже что бага). + +#### Apply 7_06_HW6_optional_jdbc.patch +> - реализовал в `JdbcUserRepositoryImpl.getAll()` доставание ролей через лямбду +> - в `insertRoles` поменял метод `batchUpdate` и сделал проверку на empty + +Еще интересные JDBC реализации: + - в `getAll()/ get()/ getByEmail()` делать запросы с `LEFT JOIN` и сделать `ResultSetExtractor` + - подключить зависимость `spring-data-jdbc-core`. Там есть готовый `OneToManyResultSetExtractor`. Можно посмотреть, как он реализован. + - реализация, зависимая от БД: доставать агрегированные роли и делать им `split(",")`: +``` +SELECT u.*, string_agg(r.role, ',') AS roles +FROM users u + JOIN user_roles r ON u.id=r.user_id +GROUP BY u.id +``` +## Занятие 7: +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Тестирование Spring MVC +#### Apply 7_07_controller_test.patch +> - в `MockMvc` добавился `CharacterEncodingFilter` +> - добавил `AllActiveProfileResolver` +> - реализация Ehcache нетранзакционная, после отката транзакции в тестах по `@Transactional` Hibernate кэш не восстанавливал роль для USER. Добавил очистку кэша Hibernate в `AbstractControllerTest.setUp` (с учетом того, что `Profiles.REPOSITORY_IMPLEMENTATION` можно переключить на `JDBC`). +- Hamcrest +- Unit Testing of Spring MVC Controllers +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. REST контроллеры +#### Apply 7_08_rest_controller.patch + +- JSON (JavaScript Object Notation +- Understanding REST +- JAX-RS vs Spring MVC +- Request mapping +- Ресурсы: + - RESTful API для сервера – делаем правильно (Часть 1) + - RESTful API для сервера – делаем правильно (Часть 2) + - И. Головач. RestAPI + - [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) + - 10 Best Practices for Better RESTful +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Тестирование REST контроллеров. Jackson. +#### Apply 7_09_rest_test_jackson.patch +- https://github.com/FasterXML/jackson-databind - - - - - - - - - - - - - - - - - - - - - - - - topjava/lesson07.md at doc · JavaWebinar/topjava - - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Кастомизация Jackson Object Mapper - +#### Apply 7_10_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) 7. [Тестирование REST контроллеров через JSONassert](https://drive.google.com/open?id=1cXbMXGxJkpI_-WNS77axG-vcfjAJ_gHg) +#### Apply 7_11_json_assert_tests.patch +- [JSONassert](https://github.com/skyscreamer/JSONassert) +- [Java Code Examples for ObjectMapper](https://www.programcreek.com/java-api-examples/index.php?api=com.fasterxml.jackson.databind.ObjectMapper) - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Тестирование через SoapUi. UTF-8 +#### Apply 7_12_soapui_utf8_converter.patch +- Инструменты тестирования REST: + - SoapUi + - Написание HTTP-запросов с помощью Curl +(для Windows можно использовать Git Bash) + - Postman + - IDEA: Tools->Test RESTful Web Service + - [Insomnia REST client](https://insomnia.rest/) - - - - +**Импортировать проект в SoapUi из config\Topjava-soapui-project.xml. Response смотреть в формате JSON.** - - +- Проверка UTF-8: http://localhost:8080/topjava/rest/profile/text +- ResponseBody and UTF-8 +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> При выполнении тестов через MockMvc никаких изменений на базе не видно, почему оно не сохраняет? +`AbstractControllerTest` аннотируется `@Transactional` - это означает, что тесты идут в транзакции и после каждого теста JUnit делает rollback базы. +> Что получается в результате выполнения запроса `SELECT DISTINCT(u) FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email`? В чем разница в SQL без `DISTINCT`. - +Запросы можно посмотреть в логах - SQL тот же. Те `DISTINCT` влияет на то, как Hibernate обрабатывает дублирующие записи (с `DISTINCT` их исключает). Результат можно посмотреть в тестах или приложении, поставив брекпойнт. +> В чем заключается расширение функциональности hamcrest в нашем тесте, что нам пришлось его отдельно от JUnit прописывать? - - - - - - - - - - - - - - +hamcrest-all используется в проверках `RootControllerTest`: `org.hamcrest.Matchers.*` - +> Jackson мы просто подключаем в помнике и спринг будет с ним работать без любых других настроек? - - +Да, Spring смотрит в classpath и если видит там Jackson, то подключает интеграцию с ним - +> Где-то слышал, что любой ресурс по REST должен однозначно идентифицироваться через url, без параметров. Правильно ли задавать URL для фильтрации в виде `http://localhost/topjava/rest/meals/filter/{startDate}/{startTime}/{endDate}/{endTime}` ? +Так делают, только при отношении агрегация, например если давать админу право смотреть еду любого юзера, URL мог бы быть похож на `http://localhost/topjava/rest/users/{userId}/meals/{mealId}`. В случае критериев, поиска или странчных данных они передаются как параметр. - +> Что означает конструкция в `JsonUtil`: `reader.readValues(json)`; +См. Generic Methods. Когда компилятор не может вывести тип, можно его уточнить при вызове generic метода. Не важно static или нет. - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW07 +- 1: Добавить тесты контроллеров: + - 1.1 `RootControllerTest.testMeals` для `meals.jsp` + - 1.2 `ResourceControllerTest` для `style.css` (проверить status и ContentType) +- 2: Реализовать `MealRestController` и протестировать его через `MealRestControllerTest` + - 2.1 cледите чтобы url в тестах совпадал с параметрами в методе контроллера. Можно добавить логирование `` для проверки маршрутизации. + - 2.2 в параметрах `getBetween` принимать `LocalDateTime` (конвертировать через @DATETIMEFORMAT WITH JAVA 8 DATE-TIME API), а передавать в тестах в формате `ISO_LOCAL_DATE_TIME` (например `'2011-12-03T10:15:30'`). Вызывать `super.getBetween()` пока без проверки на `null`, используя `toLocalDate()/toLocalTime()` (см. Optional п.3) +#### Optional +- 3: Переделать `MealRestController.getBetween` на параметры `LocalDate/LocalTime` c раздельной фильтрацией по времени/дате, работающий при `null` значениях (см. демо и `JspMealController.getBetween`). Заменить `@DateTimeFormat` на свои LocalDate/LocalTime конверторы или форматтеры. + - Spring Type Conversion + - Spring Field Formatting + - Difference between Spring MVC formatters and converters +- 4: Протестировать `MealRestController` (SoapUi, curl, IDEA Test RESTful Web Service, Postman). Запросы `curl` занести в отдельный `md` файл (либо `README.md`) -
    - - - 9855578 - - Dec 26, 2017 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    - -
    - -
    - 155 lines (116 sloc) - - 16.8 KB -
    -
    - - -
    -

    Онлайн проекта Topjava

    -

    Материалы занятия

    -

    hw Разбор домашнего задания HW6

    -

    video 1. HW6

    -

    Apply 7_01_HW6_fix_tests.patch

    -
    -
      -
    • Добавил AbstractServiceTest.isJpaBased() и Assume.assumeTrue(isJpaBased()) в AbstractMealServiceTest.testValidation().
    • -
    • Как вариант можно было вместо наследования от AbstractJpaUserServiceTest сделать @Autowired(required = false) JpaUtil (в этом случае, если в профиле jpa/datajpa не объявить JpaUtil, тестах будет NPE)
    • -
    • В новой версии Spring классы spring-mvc требуют WebApplicationContext, поэтому поправил mock.xml
    • -
    -
    -

    Apply 7_02_HW6_meals.patch

    -

    При переходе на AJAX JspMealController удалим за ненадобностью, возвращение всей еды meals() останется в RootController.

    -

    Apply 7_03_HW6_fix_relative_url_utf8.patch

    - -

    video 2. HW6 Optional

    -

    Apply 7_04_HW6_optional_add_role.patch

    -

    Apply 7_05_fix_hint_graph.patch

    -
      -
    • В JpaUserRepositoryImpl.getByEmail DISTINCT попадает в запрос, хотя он там не нужен. Это просто указание Hibernate не дублировать данные. -Для оптимизации можно указать Hibernate делать запрос без distinct: 15.16.2. Using DISTINCT with entity queries
    • -
    • Тест DataJpaUserServiceTest.testGetWithMeals() не работает для admin (у админа 2 роли и еда при JOIN дублируется). DISTINCT при несколький JOIN не помогает. -Оставил в графе только meals. Корректно поставить тип LOAD, чтобы остальные ассоциации доставались по стратегии модели. Однако с типом по умолчанию FETCH роли также достаются (похоже что бага).
    • -
    -

    Apply 7_06_HW6_optional_jdbc.patch

    -
    -
      -
    • реализовал в JdbcUserRepositoryImpl.getAll() доставание ролей через лямбду
    • -
    • в insertRoles поменял метод batchUpdate и сделал проверку на empty
    • -
    -
    -

    Еще интересные JDBC реализации:

    -
      -
    • в getAll()/ get()/ getByEmail() делать запросы с LEFT JOIN и сделать ResultSetExtractor
    • -
    • подключить зависимость spring-data-jdbc-core. Там есть готовый OneToManyResultSetExtractor. Можно посмотреть, как он реализован.
    • -
    • реализация, зависимая от БД: доставать агрегированные роли и делать им split(","):
    • -
    -
    SELECT u.*, string_agg(r.role, ',') AS roles
    -FROM users u
    -  JOIN user_roles r ON u.id=r.user_id
    -GROUP BY u.id
    -
    -

    Занятие 7:

    -

    video 3. Тестирование Spring MVC

    -

    Apply 7_07_controller_test.patch

    -
    -
      -
    • в MockMvc добавился CharacterEncodingFilter
    • -
    • добавил AllActiveProfileResolver
    • -
    • реализация Ehcache нетранзакционная, после отката транзакции в тестах по @Transactional Hibernate кэш не восстанавливал роль для USER. Добавил очистку кэша Hibernate в AbstractControllerTest.setUp (с учетом того, что Profiles.REPOSITORY_IMPLEMENTATION можно переключить на JDBC).
    • -
    -
    - -

    video 4. REST контроллеры

    -

    Apply 7_08_rest_controller.patch

    - -

    video 5. Тестирование REST контроллеров. Jackson.

    -

    Apply 7_09_rest_test_jackson.patch

    - -

    video 6. Кастомизация Jackson Object Mapper

    -

    Apply 7_10_jackson_object_mapper.patch

    - -

    video 7. Тестирование REST контроллеров через JSONassert

    -

    Apply 7_11_json_assert_tests.patch

    - -

    video 8. Тестирование через SoapUi. UTF-8

    -

    Apply 7_12_soapui_utf8_converter.patch

    - -

    Импортировать проект в SoapUi из config\Topjava-soapui-project.xml. Response смотреть в формате JSON.

    - -

    question Ваши вопросы

    -
    -

    При выполнении тестов через 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 влияет на то, как Hibernate обрабатывает дублирующие записи (с DISTINCT их исключает). Результат можно посмотреть в тестах или приложении, поставив брекпойнт.

    -
    -

    В чем заключается расширение функциональности hamcrest в нашем тесте, что нам пришлось его отдельно от JUnit прописывать?

    -
    -

    hamcrest-all используется в проверках RootControllerTest: org.hamcrest.Matchers.*

    -
    -

    Jackson мы просто подключаем в помнике и спринг будет с ним работать без любых других настроек?

    -
    -

    Да, Spring смотрит в classpath и если видит там Jackson, то подключает интеграцию с ним

    -
    -

    Где-то слышал, что любой ресурс по REST должен однозначно идентифицироваться через url, без параметров. Правильно ли задавать URL для фильтрации в виде http://localhost/topjava/rest/meals/filter/{startDate}/{startTime}/{endDate}/{endTime} ?

    -
    -

    Так делают, только при отношении агрегация, например если давать админу право смотреть еду любого юзера, URL мог бы быть похож на http://localhost/topjava/rest/users/{userId}/meals/{mealId}. В случае критериев, поиска или странчных данных они передаются как параметр.

    -
    -

    Что означает конструкция в JsonUtil: reader.<T>readValues(json);

    -
    -

    См. Generic Methods. Когда компилятор не может вывести тип, можно его уточнить при вызове generic метода. Не важно static или нет.

    -

    hw Домашнее задание HW07

    -
      -
    • 1: Добавить тесты контроллеров: -
        -
      • 1.1 RootControllerTest.testMeals для meals.jsp
      • -
      • 1.2 ResourceControllerTest для style.css (проверить status и ContentType)
      • -
      -
    • -
    • 2: Реализовать MealRestController и протестировать его через MealRestControllerTest -
        -
      • 2.1 cледите чтобы url в тестах совпадал с параметрами в методе контроллера. Можно добавить логирование <logger name="org.springframework.web" level="debug"/> для проверки маршрутизации.
      • -
      • 2.2 в параметрах getBetween принимать LocalDateTime (конвертировать через @DATETIMEFORMAT WITH JAVA 8 DATE-TIME API), а передавать в тестах в формате ISO_LOCAL_DATE_TIME (например '2011-12-03T10:15:30'). Вызывать super.getBetween() пока без проверки на null, используя toLocalDate()/toLocalTime() (см. Optional п.3)
      • -
      -
    • -
    -

    Optional

    -
      -
    • 3: Переделать MealRestController.getBetween на параметры LocalDate/LocalTime c раздельной фильтрацией по времени/дате, работающий при null значениях (см. демо и JspMealController.getBetween). Заменить @DateTimeFormat на свои LocalDate/LocalTime конверторы или форматтеры. - -
    • -
    • 4: Протестировать MealRestController (SoapUi, curl, IDEA Test RESTful Web Service, Postman). Запросы curl занести в отдельный md файл (либо README.md)
    • -
    -

    На следующем занятии используется JavaScript/jQuery. Если у вас там пробелы, пройдите его основы

    -
    -

    error Подсказки по HW07

    -
      -
    • 1: Ошибка в тесте Invalid read array from JSON обычно расшифровывается немного ниже: читайте внимательно.
    • -
    • 2: Jackson и неизменяемые объекты
    • -
    • 3: Jackson JSON Tutorial
    • -
    • 4: Если у meal, приходящий в контроллер, поля null, проверьте @RequestBody перед параметром (данные приходят в формате JSON)
    • -
    • 5: При проблемах с собственным форматтером убедитесь, что в конфигурации <mvc:annotation-driven... не дублируется
    • -
    • 6: Проверьте выполение ВСЕХ тестов через maven. В случае проблем проверьте, что не портите константу из MealTestData
    • -
    • 7: @Autowired в тестах нужно делать в том месте, где класс будет использоваться. Общий принцип: не размазывать код по классам, объявление переменных держать как можно ближе к ее использованию, группировать (не смешивать) код с разной функциональностью.
    • -
    • 8: Попробуйте в RootControllerTest.testMeals сделать сравнение через model().attribute("meals", expectedValue). Учтите, что вывод результатов через toString к сравнению отношения не имеет
    • -
    • 9: Посмотрите, нет ли в MealTestData методов, которые можно сделать общими (через generic и TestUtil)
    • -
    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +**На следующем занятии используется JavaScript/jQuery. Если у вас там пробелы, пройдите его основы** + +--------------------- +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW07 +- 1: Ошибка в тесте _Invalid read array from JSON_ обычно расшифровывается немного ниже: читайте внимательно. +- 2: Jackson и неизменяемые объекты +- 3: Jackson JSON Tutorial +- 4: Если у meal, приходящий в контроллер, поля null, проверьте `@RequestBody` перед параметром (данные приходят в формате JSON) +- 5: При проблемах с собственным форматтером убедитесь, что в конфигурации `Topjava + +## Материалы занятия + +- **Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере по Ctrl+F5** +- **При удалении файлов не забывайте делать clean: `mvn clean package`** + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW7 + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW7 + +#### Apply 8_01_HW07_controller_test.patch +> В `RootControllerTest.testMeals()` сделал проверку через `model().attribute("meals", expectedValue)`. Сравнение происходит через `MealWithExceed.equals()`, который мы можем переопределить, т.к. `MealWithExceed` не является сущностью (entity). + +#### Apply 8_02_HW07_rest_controller.patch +> - Делаем сравнения через `JSONAssert` +> - Вынес дженерик методы `contentJson() / contentJsonArray()` в `TestUtil`. Они будут общими для всех и пригодятся, если сущностей в проекте будет больше 2х. + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW7_Optional +#### Apply 8_03_HW07_formatters.patch +> Перенес форматтеры в подпакет `web`, тк они используются Spring MVC + +#### Apply 8_04_HW07_soapui_curl.patch +> Добавил примеры запросов curl в `config/curl.md` + + - Написание HTTP-запросов с помощью Curl (для Windows можно использовать Git Bash) + +## Занятие 8: + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. WebJars. jQuery and JavaScript frameworks +#### Apply 8_05_webjars.patch +> - Обновил jQuery до 3.x и остальные зависимости +> - Новое в jQuery 3 +> - УБРАЛ из проекта Dandelion обертку к datatables: +> - не встречал нигде, кроме Spring Pet Clinic; +> - поддержка работы с datatables через Dandelion оказалось гораздо более трудоемкое, чем работа с плагином напрямую. +> - Исключил из зависимостей webjars ненужные jQuery + +- Подключение веб ресурсов. WebJars. +- Introducing WebJars +- Document Object Model (DOM) +- What is the DOM? +- jQuery +- Is jquery a javascript library or framework +- DataTables +- Working with jQuery DataTables + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Bootstrap +#### Apply 8_06_bootstrap.patch +> - Добавил Glyphicons +> - В таблице удаление/редактирование сделал без кнопок (линками) + +- Подключаем Bootstrap. +- Twitter Bootstrap Tutorial +- Bootstrap 3 Tutorial +- Видео: уроки по Bootstrap 3 +- Дополнительно + - [Bootstrap верстка современного сайта за 45 минут](https://www.youtube.com/watch?v=46q2eB7xvXA) + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. AJAX. Datatables. jQuery + JSP полезны, если надо с сервера отдать статический html с серверной логикой (условия, циклы), сформированный на основе модели. Для динамической отрисовки таблицы мы будем использовать REST и JSON на 9м уроке (работа с datatables через Ajax). + +#### Apply 8_07_ajax_datatables.patch +> - Сделал загрузку скриптов асинхронной (и все общие скрипты в `headTag.jsp`). Для асинхронной загрузки вынес скрипт из `users.jsp` в `userDatatables.js`. +> - [Внешние скрипты, порядок исполнения](https://learn.javascript.ru/external-script) +> - JavaScript loading modern approach +> - Поменял стили с `jquery.dataTables` на `dataTables.bootstrap` и добавил `dataTables.bootstrap.js` +> - DataTables/Bootstrap 3 integration +> - Вместо id и селектора для добавления пользователя использовал обработчик `onclick` и функцию `add()` +> - Введение в браузерные события +> - `reset()` не чистит скрытые (hidden) поля формы. Сделал очистку полей через `form.find(":input").val("")` +> - Обновил dataTables API: +> - Converting parameter names for 1.10 +> - dataTable() vs. DataTable() + +- AJAX. +- Событие $(document).ready. +- jQuery для всех. +- jQuery для начинающих. AJAX. +- jQuery для начинающих. Селекторы. +- [jQuery task from freecodecamp](https://www.freecodecamp.org/map-aside#nested-collapsejQuery) +- jQuery API +- Редактирование таблицы на основе модального окна Bootstrap. +- Javascript плагины для Bootstrap +- DataTables API + + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Notifications +#### Apply 8_08_notification.patch +> - Сделал [защиту от кэширование ajax запросов в IE](https://stackoverflow.com/a/4303862/548473) +> - Обновил API Noty (3.1.0), добавил в сообщения glyphicon +> - [Tomcat 8.5.x перестал отдавать в заголовке `statusText`](http://tomcat.apache.org/tomcat-8.5-doc/changelog.html). Отображаем просто `status` +> - RFC 7230 states that clients should ignore reason phrases in HTTP/1.1 response messages. +> - Since the reason phrase is optional, Tomcat no longer sends it (statusText). + +- Обработка ajaxError. +- jQuery notification + +## ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Добавление Spring Security +> - Правка к видео: путь в intercept-url должен быть полный: `pattern="/rest/admin/**"` +> - В Spring Security 4.x по умолчанию включен csrf (защита от межсайтовой подделки запроса). Выключил, включим на 10-м занятии. +> - В Spring Security 5.x по умолчанию пароль кодируется. Выключил, включим на 10-м занятии. +> - [Adding a Password Encoder](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#ns-password-encoder) + +#### Apply 8_09_add_security.patch +- Spring Security +- Протокол AAA +- Методы аутентификации. +- Basic access authentication +- Использование ThreadLocal +- Добавляем в проект spring-security и security filter +- Конфигурируем security context для ресурсов и REST +- Тестируем Security REST через SoapUI/ curl/ Postman/ Testing RESTful Web Services + Generate Authorization Header +- Security with Spring +- [Decode/Encode Base64 online](http://decodebase64.com/) + +`curl -v -H 'Authorization: Basic dXNlckB5YW5kZXgucnU6cGFzc3dvcmQ=' http://localhost:8080/topjava/rest/profile/meals` + +аналогична + +`curl -v --user user@yandex.ru:password http://localhost:8080/topjava/rest/profile/meals` + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы +> Что делает код? +``` +$('.delete').click(function () { + deleteRow($(this).attr("id")); + }); +``` +На все элементы DOM с классом `delete` вешается обработчик события `click` который вызывает функцию `deleteRow`. Классы в html разделяются через пробел. См. [селекторы в jQuery](http://anton.shevchuk.name/javascript/jquery-for-beginners-selectors/) +> тянет ли bootstrap за собой jQuery? +Bootstrap css это стили (форматирование), Bootstrap js зависит от jQuery: http://stackoverflow.com/questions/14608681/can-i-use-twitter-bootstrap-without-jquery#answer-14608772 +> А где реально этот путь "classpath:/META-INF/resources/webjars"? +Внутри подключаемых webjars ресурсы лежат по пути `/META-INF/resources/webjars/...` Не поленитесь посмотреть на них через Ctrl+Shift+N. +Все подключаемые jar попадают в classpath и ресурсы доступны по этому пути. - - - - - - - - - - - - - - - - - - - - - - - - topjava/lesson08.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +> У меня webjars зависимость лежит внутри ".m2\repository\org\webjars\". С чем это может быть связано? - +Maven скачивает все депенденси в local repository, который по умолчанию находится в `~/.m2`. +Каталог по умолчанию можно переопределить в `APACHE-MAVEN-HOME\conf\settings.xml`, элемент `localRepository`. - +> WEBJARS лежат вообще в другом месте WEB-INF\lib*. Биндим mapping="/webjars/*" на реальное положение jar в ware, откуда spring знает где искать наш jquery ? - - +В war в `WEB-INF/lib/*` лежат все jar, которые попадают к classpath. Spring при обращении по url `/webjars/` ищет по пути биндинга `` +по всему classpath (то же самое как распаковать все jar в один каталог) в `META-INF/resources/webjars/`. В этом месте во всех jar, которые мы подключили из webjars лежат наши ресурсы. - +> Как можно в браузере сбросить введенный пароль базовой авторизации? +Проще всего делать новый запрос в новой анонимной вкладке (`Ctrl+Shift+N` в Chrome) - +> Оптимально ли делать доступ к статическим ресурсам (css, js, html) через webjars ? - +На продакшене под нагрузкой статические ресурсы лучше всего держать не в war, а снаружи. Доступ к ним делается либо через конфигурирование Tomcat, но чаще всего через прокси, например Nginx - - +> Как по REST определяется залогиненный юзер? Аутентификация происходит при каждом запросе? -
    - Skip to content -
    +Способы RESTful Authentication. +Мы будем использовать 2: coockie + http session (на след. уроке) и Basic Authentication с аутентификацией при каждом запросе. - - - +> Почему `@RequestParam` не работает в PUT и DELETE запросах? +По спецификации Servlet API параметры в теле для PUT, DELETE, TRACE методах не обрабатываются (только в url). +Те. можно: + - использовать POST + - передавать параметры в url + - использовать `HttpPutFormContentFilter` фильтр + - настроить Tomcat в обход спецификации. + +См. Handle request parameters for an HTTP PUT method + +> Что происходит, когда мы нажимаем на кнопку submit в форме добавления юзера (`users.jsp`, `
    `)? +В `datatablesUtil.js` берется DOM элемент (форма) по id="detailsForm" и на событие submit вешается обработчик: - -
    - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - 1492258 - - Jan 11, 2018 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    -
    - -
    - -
    - 209 lines (155 sloc) - - 20.3 KB -
    -
    - - -
    -

    Онлайн проекта Topjava

    -

    Материалы занятия

    -
      -
    • Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере по Ctrl+F5
    • -
    • При удалении файлов не забывайте делать clean: mvn clean package
    • -
    -

    hw Разбор домашнего задания HW7

    -

    video 1. HW7

    -

    Apply 8_01_HW07_controller_test.patch

    -
    -

    В RootControllerTest.testMeals() сделал проверку через model().attribute("meals", expectedValue). Сравнение происходит через MealWithExceed.equals(), который мы можем переопределить, т.к. MealWithExceed не является сущностью (entity).

    -
    -

    Apply 8_02_HW07_rest_controller.patch

    -
    -
      -
    • Делаем сравнения через JSONAssert
    • -
    • Вынес дженерик методы contentJson() / contentJsonArray() в TestUtil. Они будут общими для всех и пригодятся, если сущностей в проекте будет больше 2х.
    • -
    -
    -

    video 2. HW7_Optional

    -

    Apply 8_03_HW07_formatters.patch

    -
    -

    Перенес форматтеры в подпакет web, тк они используются Spring MVC

    -
    -

    Apply 8_04_HW07_soapui_curl.patch

    -
    -

    Добавил примеры запросов curl в config/curl.md

    -
    - -

    Занятие 8:

    -

    video 3. WebJars. jQuery and JavaScript frameworks

    -

    Apply 8_05_webjars.patch

    -
    -
      -
    • Обновил jQuery до 3.x и остальные зависимости - -
    • -
    • УБРАЛ из проекта Dandelion обертку к datatables: -
        -
      • не встречал нигде, кроме Spring Pet Clinic;
      • -
      • поддержка работы с datatables через Dandelion оказалось гораздо более трудоемкое, чем работа с плагином напрямую.
      • -
      -
    • -
    • Исключил из зависимостей webjars ненужные jQuery
    • -
    -
    - -

    video 4. Bootstrap

    -

    Apply 8_06_bootstrap.patch

    -
    -
      -
    • Добавил Glyphicons
    • -
    • В таблице удаление/редактирование сделал без кнопок (линками)
    • -
    -
    - -

    video 5. AJAX. Datatables. jQuery

    -

    JSP полезны, если надо с сервера отдать статический html с серверной логикой (условия, циклы), сформированный на основе модели. Для динамической отрисовки таблицы мы будем использовать REST и JSON на 9м уроке (работа с datatables через Ajax).

    -

    Apply 8_07_ajax_datatables.patch

    -
    - -
    - -

    video 6. Notifications

    -

    Apply 8_08_notification.patch

    -
    - -
    - -

    video 7. Добавление Spring Security

    -
    -
      -
    • Правка к видео: путь в intercept-url должен быть полный: pattern="/rest/admin/**"
    • -
    • В Spring Security 4.x по умолчанию включен csrf (защита от межсайтовой подделки запроса). Выключил, включим на 10-м занятии.
    • -
    • В Spring Security 5.x по умолчанию пароль кодируется. Выключил, включим на 10-м занятии. - -
    • -
    -
    -

    Apply 8_09_add_security.patch

    - -

    curl -v -H 'Authorization: Basic dXNlckB5YW5kZXgucnU6cGFzc3dvcmQ=' http://localhost:8080/topjava/rest/profile/meals

    -

    аналогична

    -

    curl -v --user user@yandex.ru:password http://localhost:8080/topjava/rest/profile/meals

    -

    question Ваши вопросы

    -
    -

    Что делает код?

    -
    -
    $('.delete').click(function () {
    -        deleteRow($(this).attr("id"));
    -    });
    -
    -

    На все элементы DOM с классом delete вешается обработчик события click который вызывает функцию deleteRow. Классы в html разделяются через пробел. См. селекторы в jQuery

    -
    -

    тянет ли bootstrap за собой jQuery?

    -
    -

    Bootstrap css это стили (форматирование), Bootstrap js зависит от jQuery: http://stackoverflow.com/questions/14608681/can-i-use-twitter-bootstrap-without-jquery#answer-14608772

    -
    -

    А где реально этот путь "classpath:/META-INF/resources/webjars"?

    -
    -

    Внутри подключаемых webjars ресурсы лежат по пути /META-INF/resources/webjars/... Не поленитесь посмотреть на них через Ctrl+Shift+N. -Все подключаемые jar попадают в classpath и ресурсы доступны по этому пути.

    -
    -

    У меня webjars зависимость лежит внутри ".m2\repository\org\webjars". С чем это может быть связано?

    -
    -

    Maven скачивает все депенденси в local repository, который по умолчанию находится в ~/.m2. -Каталог по умолчанию можно переопределить в APACHE-MAVEN-HOME\conf\settings.xml, элемент localRepository.

    -
    -

    WEBJARS лежат вообще в другом месте WEB-INF\lib*. Биндим mapping="/webjars/*" на реальное положение jar в ware, откуда spring знает где искать наш jquery ?

    -
    -

    В war в WEB-INF/lib/* лежат все jar, которые попадают к classpath. Spring при обращении по url /webjars/ ищет по пути биндинга <mvc:resources mapping="/webjars/ " location="classpath:/META-INF/resources/webjars/"/> -по всему classpath (то же самое как распаковать все jar в один каталог) в META-INF/resources/webjars/. В этом месте во всех jar, которые мы подключили из webjars лежат наши ресурсы.

    -
    -

    Как можно в браузере сбросить введенный пароль базовой авторизации?

    -
    -

    Проще всего делать новый запрос в новой анонимной вкладке (Ctrl+Shift+N в Chrome)

    -
    -

    Оптимально ли делать доступ к статическим ресурсам (css, js, html) через webjars ?

    -
    -

    На продакшене под нагрузкой статические ресурсы лучше всего держать не в war, а снаружи. Доступ к ним делается либо через конфигурирование Tomcat, но чаще всего через прокси, например Nginx

    -
    -

    Как по REST определяется залогиненный юзер? Аутентификация происходит при каждом запросе?

    -
    -

    Способы RESTful Authentication. -Мы будем использовать 2: coockie + http session (на след. уроке) и Basic Authentication с аутентификацией при каждом запросе.

    -
    -

    Почему @RequestParam не работает в PUT и DELETE запросах?

    -
    -

    По спецификации Servlet API параметры в теле для PUT, DELETE, TRACE методах не обрабатываются (только в url). -Те. можно:

    -
      -
    • использовать POST
    • -
    • передавать параметры в url
    • -
    • использовать HttpPutFormContentFilter фильтр
    • -
    • настроить Tomcat в обход спецификации.
    • -
    -

    См. Handle request parameters for an HTTP PUT method

    -
    -

    Что происходит, когда мы нажимаем на кнопку submit в форме добавления юзера (users.jsp, <form id="detailsForm">)?

    -
    -

    В datatablesUtil.js берется DOM элемент (форма) по id="detailsForm" и на событие submit вешается обработчик:

    -
    $('#detailsForm').submit(function () {
    -    save();
    -    return false;
    -});
    -
    -
    -

    Данные между браузером и ajax гоняются в виде json? Почему в AdminAjaxController у методов delete и createOrUpdate нет в аннотациях параметра consumes = MediaType.APPLICATION_JSON_VALUE ?

    -
    -

    Посмотреть на данные между приложением и браузером можно (и нужно!) в браузере (вкладка Network в Инструментах разработчика, F12 в Хроме). Зависит от того, как их отправляем из браузера и из приложения. Данные формы обычно передаются просто параметрами. APPLICATION_JSON_VALUE в контроллере нужно, только если параметры отдаются\принимаются в формате JSON.

    -

    hw Домашнее задание HW08

    -
      -
    • 1: Перевести meals на datatables (meals.jsp, MealAjaxController). -
        -
      • 1.1 Реализовать добавление записи еды через модальное окно Bootstrap и удаление еды по ajax (БЕЗ редактирования).
      • -
      • 1.2 При вставке данных по AJAX пропадает все JSP форматирование, чинить перерисовку НЕ надо. Следующий урок- будем делать datatable по AJAX и форматирование на стороне клиента.
      • -
      -
    • -
    • 2: Т.к. HTML атрибут id у каждого элемента документа должен быть уникален, нужно избавиться от дублирования id="${user.id}" в строках таблиц users и meals (переместить атрибут id в тэг <tr> или передавать в качестве параметра функций через onclick)
    • -
    -

    Optional.

    -
      -
    • 3: Перевести работу фильтра на AJAX. Попробуйте после модификации таблицы (например добавлении записи) обновлять ее также с учетом фильтра.
    • -
    • 4: Сделать кнопку сброса фильтра.
    • -
    • 5: Реализовать enable/disable User через checkbox в users.jsp с сохранением в DB. Неактивных пользователей выделить css стилем.
    • -
    -
    -

    error Подсказки по HW08

    -
      -
    • 1: проверьте, как у вас первоначально (или по F5) отображаются неактивные пользователи (если меняете css при enable/disable)
    • -
    • 2: если у вас запрос фильтрации не попадает в Ajax контроллер, скорее всего срабатывает submit формы фильтрации. Нужно или делать в событии return false как в datatablesUtil:$('#detailsForm').submit, либо вместо type="submit" сделать type="button"
    • -
    • 3: enable/disable делать c @Transactional (можно реализовать как на уровне репозитория, так и на уровне сервиса через несколько sql, которые должны быть в одной транзакции)
    • -
    • 4: в datatablesUtil.js следует выносите только общие скрипты (cкрипты еды размещайте в mealDatatables.js, пользователей в userDatatables.js)
    • -
    • 5: если в контроллер приходит null проверте в Network вкладке браузера в каком формате приходят данные и в каком формате в контроллере вы их принимаете (consumes).
    • -
    • 6: при реализации enable/disable лучше явно указывать нужное состояние, чем переключать на противоположное. Если параллельно вам кто-то изменит состояние, то будет несоответствие UI и DB.
    • -
    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - +#### Optional. +- 3: Перевести работу фильтра на AJAX. Попробуйте после модификации таблицы (например добавлении записи) обновлять ее также с учетом фильтра. +- 4: Сделать кнопку сброса фильтра. +- 5: Реализовать enable/disable User через checkbox в `users.jsp` с сохранением в DB. Неактивных пользователей выделить css стилем. +--------------------- +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Подсказки по HW08 +- 1: проверьте, как у вас первоначально (или по F5) отображаются неактивные пользователи (если меняете css при enable/disable) +- 2: если у вас запрос фильтрации не попадает в Ajax контроллер, скорее всего срабатывает submit формы фильтрации. Нужно или делать в событии `return false` как в `datatablesUtil:$('#detailsForm').submit`, либо вместо `type="submit"` сделать `type="button"` +- 3: enable/disable делать c `@Transactional` (можно реализовать как на уровне репозитория, так и на уровне сервиса через несколько sql, которые должны быть в одной транзакции) +- 4: в `datatablesUtil.js` следует выносите только общие скрипты (cкрипты еды размещайте в `mealDatatables.js`, пользователей в `userDatatables.js`) +- 5: если в контроллер приходит `null` проверте в `Network` вкладке браузера в каком формате приходят данные и в каком формате в контроллере вы их принимаете (`consumes`). +- 6: при реализации `enable/disable` лучше явно указывать нужное состояние, чем переключать на противоположное. Если параллельно вам кто-то изменит состояние, то будет несоответствие UI и DB. \ No newline at end of file diff --git a/doc/lesson09.md b/doc/lesson09.md index fb550263ad4c..f04063ecebe7 100644 --- a/doc/lesson09.md +++ b/doc/lesson09.md @@ -1,978 +1,185 @@ +# Онлайн проекта Topjava +## Материалы занятия +- **Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере по Ctrl+F5** +- **При удалении файлов не забывайте делать clean: `mvn clean package`** +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правка +#### Apply 9_0_fix.patch +- Обновил некоторые зависимости +- Для DELETE сделал возвращаемый статус `NO_CONTENT (204)` + - [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/) +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW8 - - - - - - - - - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW8 +**Поправка к видео: в гриде bootstrap 12 колонок.** +#### Apply 9_01_HW8.patch +> - Все события сделал через `onlick`. Чтобы формы не сабмитились, заменил `type="submit"` на `type="button"`. +> - Фильтр еды сделал в [Bootstrap Panels](http://getbootstrap.com/components/#panels) +> - [Fitting a panel into bootstrap's grid system](http://stackoverflow.com/questions/24816175) +> - Удалил лишние классы, JSP и i18N +- Grid system +- Bootstrap description +- Bootstrap forms - - - - - - +#### Apply 9_02_HW8_clear_filter.patch +> Добавил сброс фильтра - - - topjava/lesson09.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW8 Optional (enable/disable user) - +#### Apply 9_03_HW8_enable_disable.patch +> - Перенес метод `enable` в `userDatatables.js` и вынес стиль `disabled` в css +> - Меняю стиль `` ПОСЛЕ успешной обработки запроса через `toggleClass` и при ошибке возвращаю `checked` в прежнее состояние +> - Убрал `init()`. При переводе таблицы на Ajax вместо него будет `createdRow`. Стили `disabled` добавляются при отрисовки таблицы в JSP - - - - +## Занятие 9: - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. Spring Binding +#### Apply 9_04_binding.patch +> Move `ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY` to `ru.javawebinar.topjava.util.UserUtil` +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Реализация update +#### Apply 9_05_update.patch +> - Сделал интерфейс `HasId` от которого наследуются `BaseTo` и `AbstractBaseEntity` +> - Сделал проверку `id` в `ValidationUtil` на основе `HasId` +> - Сделал в `ProfileRestController` обновление своего профиля через `UserTo` (нельзя изменять себе роли) и поправил тест +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: +> Можно ли было удаление делать без перезагрузки таблицы (удалением строки) и для редактирования брать данные со страницы, а не с сервера? - +В многопользовательском приложении принято при изменении данных подтягивать все изменения с базы, иначе может быть большая несогласованность базы и UI. В таблице еды наши пользователи видят только свои записи, но лучше для всех таблиц делать общий подход. Дополнительная нагрузка на базу тут совсем небольшая. +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Spring Validation +#### Apply 9_06_validation.patch +> - заменил hibernate `@Depricated` аннотации валидации на анологичные javax +> - `responseJSON` не выводится в случае его отсутствия (например при попытке добавить пользователья с дублирующимся email) +> - сделал конкатенацию ошибок через `StringJoiner` +> - при неверном формате email делается проверка `startsWith`, чтобы поле email не дублировалось в сообщении - +- Spring Validation. +- Bean Validation +- Валидация формы по AJAX. +- JSR-303, 349 +- @Valid @RequestBody + Error handling - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Перевод DataTables на Ajax +#### Apply 9_07_datatable_via_ajax.patch +> Напомню, что мы перешли на [параметры Datatables в формате 1.10](https://datatables.net/upgrade/1.10-convert) - - +- [DataTables Ajax](https://datatables.net/manual/ajax) - +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: +> Что за дополнительный параметр (который каждый раз инкрементируется) появляется при запросе datatables данных по ajax (например `http://localhost:8080/topjava/ajax/admin/users/?_=1496156621129`) ? - +Это защита `datatables` от кэширования запроса браузером (например в IE). - - +#### Apply 9_08_js_i18n.patch +> - Добавил [простую интернационализацию в JavaScript](https://stackoverflow.com/questions/6218970/resolving-springmessages-in-javascript-for-i18n-internationalization). +> - на стороне сервера формируется `i18n` JavaScript массив с значениями, который затем используется для интернационализации в браузере +> - в модальном окне заголовок подменяется через `$('#modalTitle').html(..title)` + +#### Для тестирования локали [можно поменять `Accept-Language`](https://stackoverflow.com/questions/7769061/how-to-add-custom-accept-languages-to-chrome-for-pseudolocalization-testing). Для хрома в `chrome://settings/languages` перетащить нужную локаль наверх. + +- JavaScript internationalization + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Форма логина / логаут. +#### Apply 9_09_min_form_login.patch + +> Добавил функциональность logout + +- Минимальный form-login +- Migrating <form-login> + +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Мои вопросы: +- Почему при логине как admin еда отдаются для user? +- Почему при логине как user не отображается список пользователей? +- Почему еда не редактируется? + +> Подсказка: поглядите вкладку Network в браузере. + +#### Apply 9_10_jsp_form_login.patch +> Рефакторинг +> - В `login.jsp` вместо атрибутов достаю параметры запроса (`param.error/message`). +> - Закрыл доступ к `/login` для уже авторизованных в приложении пользователей (в `spring-security.xml` изменил `permitAll` на `isAnonymous`) + +- Собственный form-login - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Реализация собственного провайдера авторизации. +#### Apply 9_11_auth_via_user_service.patch - - +- UserDetailsService Implementations - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Принцип работы Spring Security. Проксирование. +- Принцип работы Spring Security +- Типы проксирования +- Dynamic Proxy API +- Конфликт проксирования Data Repository +- Security фильтры +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 10. Spring Security Test +#### Apply 9_12_spring_security_test.patch +> - Cделал "честную" авторизацию в `RootControllerTest` (через `authentication` в утильном методе `TestUtil`) +> - Cделал `mockAuthorize` для `SpringMain`, в который не попадают фильтры + +- Spring Security Test +- Интеграция со Spring MVC Test +- HttpBasic авторизация +- Тестирование контроллеров с помощью MockMvc (без spring-security-test) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 11. Cookie. Session. +- HTTP cookie +- Under what conditions is a JSESSIONID created? +- Tomcat Session Serialization + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> В куки попадает обычная строка JSESSIONID. Куда сериалиуется объект User? + +Для хранения сосотяния сессии (например корзины покупателя) в Servlet API есть механизм хранения объектов сессии (грубо- мультимапмапа, которая достается из хранилища по ключу). При создании сессии на стороне сервера (через `request.getSession`) создается кука `JSESSIONID`, которая пеердается между клиентом и сервером и является ключом в хранилище объектов сессий. См. обработка сессий с помощью сервлетов + +> В `login.jsp` есть форма `<form:form action="spring_security_check" ..>` Где такой url используется? + +Это стандартный url для авторизации в `spring-security`. Он его и обрабатывает. + +> Если не пользовать js, а писать UI на JSP, сообщения между ui и сервером будут в формате json? Это же будет JSON API? + +Есть данные, которые передаются между клиентом и сервером в формате json или get/post с параметрами, есть стили взаимодействия клиента и сервера (REST, JSON API, JSON-RPC) и есть отрисовка UI: JSP, Javascript фреймворк, Thymleaf и пр. Не надо эти вещи путать между собой. + +> По умолчанию спринг работает с `UserDetailsService`, который должен возвращать `UserDetails`. Но мы не хотим стандартные, мы хотим свои, поэтому просто наследуем наши `UserServiceImpl` и `AuthorizedUser` от соответствующих интерфейсов и реализуем недостающие методы, которые spring security и будет использовать? + +Да. Сервис аутентификации конфигурится в `spring-security.xml` `` и должен реализовывать интерфейс `UserDetailsService`. В spring-security есть его стандартные реализации, которые использовались до нашей кастомной `UserServiceImpl`, например `jdbc-user-service` использует реализацию `JdbcUserDetailsManager` + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW9 + +- 1: Реализовать для meal Binding/ Update/ Validation. Проверить работу при пустом значении `calories`. +- 2: Перевести `meals.jsp` на работу по ajax. Стиль строки таблицы сделать в зависимости от exceeded, время отображать без `T`. Добавить i18n. +- 3: Починить meals тесты, добавить тест на неавторизованный доступ + +#### Optional +- 4: Подключить datetime-picker к фильтрам и модальному окну добавления/редактирования еды + - 4.1 DateTimePicker jQuery plugin + - 4.2 Еще есть Bootstrap 3 Datepicker -
    - - - 6aef295 - - Jan 9, 2018 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    - -
    - -
    - 186 lines (131 sloc) - - 18.8 KB -
    -
    - - -
    -

    Онлайн проекта Topjava

    -

    Материалы занятия

    -
      -
    • Браузер кэширует javascript и css. Если изменения не работают, обновите приложение в браузере по Ctrl+F5
    • -
    • При удалении файлов не забывайте делать clean: mvn clean package
    • -
    -

    error Правка

    -

    Apply 9_0_fix.patch

    - -

    hw Разбор домашнего задания HW8

    -

    video 1. HW8

    -

    Поправка к видео: в гриде bootstrap 12 колонок.

    -

    Apply 9_01_HW8.patch

    -
    -
      -
    • Все события сделал через onlick. Чтобы формы не сабмитились, заменил type="submit" на type="button".
    • -
    • Фильтр еды сделал в Bootstrap Panels - -
    • -
    • Удалил лишние классы, JSP и i18N
    • -
    -
    - -

    Apply 9_02_HW8_clear_filter.patch

    -
    -

    Добавил сброс фильтра

    -
    -

    video 2. HW8 Optional (enable/disable user)

    -

    Apply 9_03_HW8_enable_disable.patch

    -
    -
      -
    • Перенес метод enable в userDatatables.js и вынес стиль disabled в css
    • -
    • Меняю стиль <tr> ПОСЛЕ успешной обработки запроса через toggleClass и при ошибке возвращаю checked в прежнее состояние
    • -
    • Убрал init(). При переводе таблицы на Ajax вместо него будет createdRow. Стили disabled добавляются при отрисовки таблицы в JSP
    • -
    -
    -

    Занятие 9:

    -

    video 3. Spring Binding

    -

    Apply 9_04_binding.patch

    -
    -

    Move ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY to ru.javawebinar.topjava.util.UserUtil

    -
    -

    video 4. Реализация update

    -

    Apply 9_05_update.patch

    -
    -
      -
    • Сделал интерфейс HasId от которого наследуются BaseTo и AbstractBaseEntity
    • -
    • Сделал проверку id в ValidationUtil на основе HasId
    • -
    • Сделал в ProfileRestController обновление своего профиля через UserTo (нельзя изменять себе роли) и поправил тест
    • -
    -
    -

    question Вопрос:

    -
    -

    Можно ли было удаление делать без перезагрузки таблицы (удалением строки) и для редактирования брать данные со страницы, а не с сервера?

    -
    -

    В многопользовательском приложении принято при изменении данных подтягивать все изменения с базы, иначе может быть большая несогласованность базы и UI. В таблице еды наши пользователи видят только свои записи, но лучше для всех таблиц делать общий подход. Дополнительная нагрузка на базу тут совсем небольшая.

    -

    video 5. Spring Validation

    -

    Apply 9_06_validation.patch

    -
    -
      -
    • заменил hibernate @Depricated аннотации валидации на анологичные javax
    • -
    • responseJSON не выводится в случае его отсутствия (например при попытке добавить пользователья с дублирующимся email)
    • -
    • сделал конкатенацию ошибок через StringJoiner
    • -
    • при неверном формате email делается проверка startsWith, чтобы поле email не дублировалось в сообщении
    • -
    -
    - -

    video 6. Перевод DataTables на Ajax

    -

    Apply 9_07_datatable_via_ajax.patch

    -
    -

    Напомню, что мы перешли на параметры Datatables в формате 1.10

    -
    - -

    question Вопрос:

    -
    -

    Что за дополнительный параметр (который каждый раз инкрементируется) появляется при запросе datatables данных по ajax (например http://localhost:8080/topjava/ajax/admin/users/?_=1496156621129) ?

    -
    -

    Это защита datatables от кэширования запроса браузером (например в IE).

    -

    Apply 9_08_js_i18n.patch

    -
    -
      -
    • Добавил простую интернационализацию в JavaScript. -
        -
      • на стороне сервера формируется i18n JavaScript массив с значениями, который затем используется для интернационализации в браузере
      • -
      • в модальном окне заголовок подменяется через $('#modalTitle').html(..title)
      • -
      -
    • -
    -
    -

    Для тестирования локали можно поменять Accept-Language. Для хрома в chrome://settings/languages перетащить нужную локаль наверх.

    - -

    video 7. Форма логина / логаут.

    -

    Apply 9_09_min_form_login.patch

    -
    -

    Добавил функциональность logout

    -
    - -

    question Мои вопросы:

    -
      -
    • Почему при логине как admin еда отдаются для user?
    • -
    • Почему при логине как user не отображается список пользователей?
    • -
    • Почему еда не редактируется?
    • -
    -
    -

    Подсказка: поглядите вкладку Network в браузере.

    -
    -

    Apply 9_10_jsp_form_login.patch

    -
    -

    Рефакторинг

    -
      -
    • В login.jsp вместо атрибутов достаю параметры запроса (param.error/message).
    • -
    • Закрыл доступ к /login для уже авторизованных в приложении пользователей (в spring-security.xml изменил permitAll на isAnonymous)
    • -
    -
    - -

    video 8. Реализация собственного провайдера авторизации.

    -

    Apply 9_11_auth_via_user_service.patch

    - -

    video 9. Принцип работы Spring Security. Проксирование.

    - -

    video 10. Spring Security Test

    -

    Apply 9_12_spring_security_test.patch

    -
    -
      -
    • Cделал "честную" авторизацию в RootControllerTest (через authentication в утильном методе TestUtil)
    • -
    • Cделал mockAuthorize для SpringMain, в который не попадают фильтры
    • -
    -
    - -

    video 11. Cookie. Session.

    - -

    question Ваши вопросы

    -
    -

    В куки попадает обычная строка JSESSIONID. Куда сериалиуется объект User?

    -
    -

    Для хранения сосотяния сессии (например корзины покупателя) в Servlet API есть механизм хранения объектов сессии (грубо- мультимапмапа, которая достается из хранилища по ключу). При создании сессии на стороне сервера (через request.getSession) создается кука JSESSIONID, которая пеердается между клиентом и сервером и является ключом в хранилище объектов сессий. См. обработка сессий с помощью сервлетов

    -
    -

    В login.jsp есть форма &lt;form:form action="spring_security_check" ..&gt; Где такой url используется?

    -
    -

    Это стандартный url для авторизации в spring-security. Он его и обрабатывает.

    -
    -

    Если не пользовать js, а писать UI на JSP, сообщения между ui и сервером будут в формате json? Это же будет JSON API?

    -
    -

    Есть данные, которые передаются между клиентом и сервером в формате json или get/post с параметрами, есть стили взаимодействия клиента и сервера (REST, JSON API, JSON-RPC) и есть отрисовка UI: JSP, Javascript фреймворк, Thymleaf и пр. Не надо эти вещи путать между собой.

    -
    -

    По умолчанию спринг работает с UserDetailsService, который должен возвращать UserDetails. Но мы не хотим стандартные, мы хотим свои, поэтому просто наследуем наши UserServiceImpl и AuthorizedUser от соответствующих интерфейсов и реализуем недостающие методы, которые spring security и будет использовать?

    -
    -

    Да. Сервис аутентификации конфигурится в spring-security.xml <authentication-manager> и должен реализовывать интерфейс UserDetailsService. В spring-security есть его стандартные реализации, которые использовались до нашей кастомной UserServiceImpl, например jdbc-user-service использует реализацию JdbcUserDetailsManager

    -

    hw Домашнее задание HW9

    -
      -
    • 1: Реализовать для meal Binding/ Update/ Validation. Проверить работу при пустом значении calories.
    • -
    • 2: Перевести meals.jsp на работу по ajax. Стиль строки таблицы сделать в зависимости от exceeded, время отображать без T. Добавить i18n.
    • -
    • 3: Починить meals тесты, добавить тест на неавторизованный доступ
    • -
    -

    Optional

    - -

    Попробуйте при запросах по REST оставить стандартный ISO формат (с разделителем T)

    -

    error Проверка в HW09

    -
      -
    • 1: Проверьте, что при добавлении и редактировании пользователя и еды у вас корректно отображаются заголовки модального окна: -"Добавить/Редактировать еду пользователя"
    • -
    • 2: Не дублируйте
    • -
    -
    <c:forEach var='key' ...
    +Попробуйте при запросах по REST оставить стандартный ISO формат (с разделителем `T`)
    +
    +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Проверка в HW09
    +- 1: Проверьте, что при добавлении и редактировании пользователя и еды у вас корректно отображаются заголовки модального окна:
    +"Добавить/Редактировать еду пользователя"
    +- 2: Не дублируйте
    +```
    +
    -
      -
    • 3: Для подключения css и js datetimepicker-а посмотрите в его jar (или поищите в проекте по Ctrl+Shift+N: datetimepicker)
    • -
    • 4: datetimepicker работает корректно в Хроме, если убрать в type в <input type="date/time/datetime-local" ..
    • -
    • 5: Если появляются проблемы с JS типа ... is not defined - обратите внимание на порядок загрузки скриптов и атрибут defer. Скрипты должны идти в нужном порядке. Если определяете скрипт прямо в jsp, он выполняется до defer скриптов.
    • -
    • 6: Не дублируйте обработку ошибок в BindingResult в ajax контроллерах
    • -
    • 7: Проверьте редактирование еды: открыть на редактирование и сохранить не должно приводить к ошибкам с форматом времени.
    • -
    • 8: Проверьте в RootController.meals(), его нужно тоже поправить
    • -
    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +``` +- 3: Для подключения css и js datetimepicker-а посмотрите в его jar (или поищите в проекте по Ctrl+Shift+N: `datetimepicker`) +- 4: datetimepicker работает корректно в Хроме, если убрать в `type` в `Topjava +## Материалы занятия +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW9 +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW9 +#### Apply 10_1_HW9_binding_ajax.patch +Datatables перевели на ajax (`"ajax": {"url": ajaxUrl, ..`), те при отрисовке таблица сама по этому url запрашивает данные. Поэтому в методе `RootController.meals()` нам нужно только возвратить view "meals" (`meals.jsp`) которому уже не нужны данные в атрибутах. +> - JavaScript `i18n[]` локализацию перенес в `i18n.jsp` +> - Вынес общий код в `ValidationUtil.getErrorResponse()` - - - - - - - - - - - +#### Apply 10_2_HW9_test.patch +#### Apply 10_3_HW9_datetimepicker.patch +> - Изменил формат ввода dateTime в форме без 'T': при биндинге значений к полям формы в `datatablesUtil.updateRow()` для поля `dateTime` делаю `replace('T', ' ')`. REST интерфейс по прежнему работает в стандарте ISO-8601 +> - Вынес общий код в `datatablesUtil.formatDate()` +- DateTimePicker jQuery plugin +- Datetimepicker and ISO-8601 - - - - - - +## Занятие 10: - - - topjava/lesson10.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - +#### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. [Дополнительно: кастомизация JSON (@JsonView) и валидации (groups)](https://drive.google.com/file/d/0B9Ye2auQ_NsFRTFsTjVHR2dXczA) +**ВНИМАНИЕ! `10_04_opt` и `10_05_opt` - не обязательные патчи. Если будете делать- то в отдельной ветке (у меня `opt_view_groups`). Это варианты решений, которые не идут в `master`.** +- [Что возвращать: Entity или DTOs](https://stackoverflow.com/a/21569720/548473) - +#### Apply 10_04_opt_json_view.patch +- [Jackson Annotation ](http://www.baeldung.com/jackson-annotations) + - [Jackson JSON Views](http://www.baeldung.com/jackson-json-view-annotation) +- [@JsonView: фильтруем JSON](https://habrahabr.ru/post/307392/) +- [Jackson set default view](https://stackoverflow.com/questions/22875642/jackson-set-default-view) - - - - +#### Apply 10_05_opt_validated_groups.patch +- [Validation Group in SpringMVC](https://narmo7.wordpress.com/2014/04/26/how-to-set-up-validation-group-in-springmvc/) +- [@Validated and Default group](http://forum.spring.io/forum/spring-projects/web/117289-validated-s-given-groups-should-consider-default-group-or-not) - - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 3. [Рефакторинг: jQuery конверторы и группы валидации по умолчанию](https://drive.google.com/file/d/1tOMOdmaP5OQ7iynwC77bdXSs-13Ommax) +#### Apply 10_04_jquery_converters.patch +- [jQuery: типы данных](https://jquery-docs.ru/jQuery.ajax/#data-types) +- [jQuery: конверторы](https://jquery-docs.ru/jQuery.ajax/#using-converters) +#### Apply 10_05_persist_validate_group.patch +- [Default Hibernate validation](https://stackoverflow.com/a/16930663/548473). Т.к. `Persist` наследуется от `javax.validation.groups.Default`, при сохранении учитываются все непомеченные аннотации валидации (`Default`) + помеченные `Persist`. - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Spring Security Taglib. Method Security Expressions. +#### Apply 10_06_secure_tag_annotation.patch +- Spring Security Taglib. +- Method Security и Method Security Expressions. - +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос: +> У нас в `sprng-mvc` подключается `spring-app`, в который подключается `spring-security`. Т.о. `spring-mvc` в себе содержит код `spring-app` и `spring-security`. Почему тогда аннотация ` - +1. Подключаем один контекст внутрь другого: это про import а не про наследование (тоже самое что скопипастить сюда xml из подключаемого import файла). Наследование означает, что поиск бина, если он не найден, происходит в родителе. +2. При создании контекста аннотация ` - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 5. Interceptors. Редактирование профиля. JSP tag files. +#### Apply 10_07_interceptor.patch +- Spring interceptors. - +#### Apply 10_08_profile_jsptag.patch - +- Делаем jsp tag для ввода поля формы. +- Spring MVC Form Validation +- Spring MVC Form: check if a field has an error - - +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 6. Форма регистрации. +#### Apply 10_09_registration.patch + +> - Добавил локализацию +> - При регистрации передаю `username` в `login.jsp` как параметр и делаю `setCredentials` (пароль вводится скрыто, поэтому его не передаю из соображений безопасности). Т.к `setCredentials` требует jQuery, убрал для него `defer` + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 7. Обработка исключений в Spring. + +#### Apply 10_10_not_found_422.patch +> [Поменял код 404 (URL not found) на 422 (Unprocessable Entity)](http://stackoverflow.com/a/22358422/548473) + +- Используем HTTP status code - +#### Apply 10_11_global_exception.patch +> - Перед отображением exception предварительно делаю `ValidationUtil.getRootCause` +> - Добавил локализацию - - +- Global Exception Handling - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - Fetching contributors… -
    - -
    - - Cannot retrieve contributors at this time -
    -
    - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    -
    - -
    - -
    - 188 lines (132 sloc) - - 19.8 KB -
    -
    - - -
    -

    Онлайн проекта Topjava

    -

    Материалы занятия

    -

    hw Разбор домашнего задания HW9

    -

    video 1. HW9

    -

    Apply 10_1_HW9_binding_ajax.patch

    -

    Datatables перевели на ajax ("ajax": {"url": ajaxUrl, ..), те при отрисовке таблица сама по этому url запрашивает данные. Поэтому в методе RootController.meals() нам нужно только возвратить view "meals" (meals.jsp) которому уже не нужны данные в атрибутах.

    -
    -
      -
    • JavaScript i18n[] локализацию перенес в i18n.jsp
    • -
    • Вынес общий код в ValidationUtil.getErrorResponse()
    • -
    -
    -

    Apply 10_2_HW9_test.patch

    -

    Apply 10_3_HW9_datetimepicker.patch

    -
    -
      -
    • Изменил формат ввода dateTime в форме без 'T': при биндинге значений к полям формы в datatablesUtil.updateRow() для поля dateTime делаю replace('T', ' '). REST интерфейс по прежнему работает в стандарте ISO-8601
    • -
    • Вынес общий код в datatablesUtil.formatDate()
    • -
    -
    - -

    Занятие 10:

    -

    video 2. Дополнительно: кастомизация JSON (@JsonView) и валидации (groups)

    -

    ВНИМАНИЕ! 10_04_opt и 10_05_opt - не обязательные патчи. Если будете делать- то в отдельной ветке (у меня opt_view_groups). Это варианты решений, которые не идут в master.

    - -

    Apply 10_04_opt_json_view.patch

    - -

    Apply 10_05_opt_validated_groups.patch

    - -

    video 3. Рефакторинг: jQuery конверторы и группы валидации по умолчанию

    -

    Apply 10_04_jquery_converters.patch

    - -

    Apply 10_05_persist_validate_group.patch

    -
      -
    • Default Hibernate validation. Т.к. Persist наследуется от javax.validation.groups.Default, при сохранении учитываются все непомеченные аннотации валидации (Default) + помеченные Persist.
    • -
    -

    video 4. Spring Security Taglib. Method Security Expressions.

    -

    Apply 10_06_secure_tag_annotation.patch

    - -

    question Вопрос:

    -
    -

    У нас в sprng-mvc подключается spring-app, в который подключается spring-security. Т.о. spring-mvc в себе содержит код spring-app и spring-security. Почему тогда аннотация <security:global-method-security... из spring-security не видна для контроллера из spring-mvc?

    -
    -
      -
    1. Подключаем один контекст внутрь другого: это про import а не про наследование (тоже самое что скопипастить сюда xml из подключаемого import файла). Наследование означает, что поиск бина, если он не найден, происходит в родителе.
    2. -
    3. При создании контекста аннотация <security:global-method-security... говорит про то, что все бины текущего контекста, у которых есть аннотации авторизации (@PreAuthorize/ @Secured/ ..) будут проксироваться с проверкой авторизации. Контексты родителей и детей создаются сами по себе и аннотации проксирования у них собственные.
    4. -
    -

    video 5. Interceptors. Редактирование профиля. JSP tag files.

    -

    Apply 10_07_interceptor.patch

    - -

    Apply 10_08_profile_jsptag.patch

    - -

    video 6. Форма регистрации.

    -

    Apply 10_09_registration.patch

    -
    -
      -
    • Добавил локализацию
    • -
    • При регистрации передаю username в login.jsp как параметр и делаю setCredentials (пароль вводится скрыто, поэтому его не передаю из соображений безопасности). Т.к setCredentials требует jQuery, убрал для него defer
    • -
    -
    -

    video 7. Обработка исключений в Spring.

    -

    Apply 10_10_not_found_422.patch

    -
    -

    Поменял код 404 (URL not found) на 422 (Unprocessable Entity)

    -
    - -

    Apply 10_11_global_exception.patch

    -
    -
      -
    • Перед отображением exception предварительно делаю ValidationUtil.getRootCause
    • -
    • Добавил локализацию
    • -
    -
    - -

    Apply 10_12_controller_advice_exception.patch

    -

    Отображение валидации для формы еды и юзера пропало (TODO в коде для HW10). Можно посмотреть сериализацию в ErrorInfo при попытке добавить юзера с дублирующимся email.

    -
    -
      -
    • Добавил в ErrorInfo тип ошибки ErrorType
    • -
    • @ResponseBody над методами ExceptionInfoHandler заменил на @RestControllerAdvice
    • -
    • В ExceptionInfoHandler удалил @Order над методами и добавил над классом:
    • -
    -
    -
      Methods are matched by closest exception in
    +#### Apply 10_12_controller_advice_exception.patch
    +**Отображение валидации для формы еды и юзера пропало (`TODO` в коде для HW10). Можно посмотреть сериализацию в `ErrorInfo` при попытке добавить юзера с дублирующимся email.**
    +
    +> - Добавил в `ErrorInfo` тип ошибки `ErrorType`
    +> - `@ResponseBody` над методами `ExceptionInfoHandler` заменил на `@RestControllerAdvice`
    +> - В `ExceptionInfoHandler` удалил `@Order` над методами и добавил над классом:
    +```
    +  Methods are matched by closest exception in
       *  @see  org.springframework.web.method.annotation.ExceptionHandlerMethodResolver#getMappedMethod
       *  164: Collections.sort(matches, new ExceptionDepthComparator(exceptionType))
    -
    -
    -
      -
    • В ExceptionInfoHandler.logAndGetErrorInfo() также использую ValidationUtil.getRootCause
    • -
    -
    - -

    video 8. Encoding password. Json READ/WRITE access

    -

    Apply 10_13_password_encoding.patch

    - -

    Apply 10_14_read_write_access.patch

    -

    В реальном приложении для управления паролем необходим отдельный UI интерфейс с подтверждением старого пароля.

    - -

    video 9. Межсайтовая подделка запроса (CSRF).

    -

    Apply 10_15_csrf.patch

    -
    -

    Убрал form:form из ajax запросов: там csrf работает через header. Проверьте в вкладке браузера Network.

    -
    -

    Поломалась UTF-8 кодировка в редактировании профиля и регистрациию (если по умолчанию не UTF-8). В Optional HW10 нужно будет починить.

    - -

    question Ваши вопросы

    -
    -

    В чем отличие между аннотоацией @PreAuthorize("hasRole('ROLE_ADMIN')") и конфигурацией в jsp: <sec:authorize access="isAuthenticated()">, <sec:authorize access="hasRole('ROLE_ADMIN')"> ?

    -
    -

    Анотация @PreAuthorize обрабатывается Spring анологично @Transactional, @Cacheable - класс проксируется и до-после вызова метода добавляется функциональность. -В данном случае перед вызовом метода проверяются роль залогиненного юзера. JSTL тэг authorize выполняет проверку условия в залогиненном юзере внутри jsp.

    -
    -

    Еще раз: почему не нужен csrf для REST и нельзя подделать JSON запрос с вредоносного сайта?

    -
    -

    Попробуйте выполнить ajax запрос из вашего приложения c url, у которого домен отличный от вашего (например "http://topjava.herokuapp.com/meals/ajax/admin/users/"+id). -В консоли браузера будет XMLHttpRequest cannot load... - нарушение same origin policy. -Формам же разрешается делать submit (через action=..) на другой домен, но невозможно cделать Content-Type, отличный от стндартных enctype и методов кроме get и post. Таким образом consumes = MediaType.APPLICATION_JSON_VALUE в POST защищает приложение от CSRF.

    -
    -

    Почему использован BCryptPasswordEncoderа не hash(password+salt). Почему нельзя напрямую сравнивать закодированные пароли?

    -
    -

    BCryptPasswordEncoder automatically generates a salt and concatenates it with the hash value in a single String. Для одного и тогоже пароля получаются разные хэши. Cравнивать их можно только через isMatch() с незахэшированным паролем.

    -
    -

    Когда запускается в GlobalControllerExceptionHandler метод defaultErrorHandler? Когда как в него исключение попадает? Как выбирается, кто обрабатывает исключения: ExceptionInfoHandler или GlobalControllerExceptionHandler?

    -
    -

    GlobalControllerExceptionHandler попадает в контекст спринг (через @ControllerAdvice его находят в пакете web). Далее спринг перехватывает исключения и отправляет в подходящий по исключению метод GlobalControllerExceptionHandler. ExceptionInfoHandler помечен @ControllerAdvice(annotations = RestController.class), он обрабатывает только ошибки из всех контроллеров с аннотацией RestController.class.

    -
    -

    Откуда берутся в валидации сообщения на русском "должно быть между 10 и 10000"?

    -
    -

    Локализация встроена в Hibernate Validation. Смотрите Ctrl+Shift+N и ValidationMessages_ru.properties.

    -

    hw Домашнее задание HW10

    -
      -
    • 1: Сделать валидацию в AdminAjaxController/MealAjaxController через ExceptionInfoHandler. Вернуть клиенту ErrorInfo и статус HttpStatus.UNPROCESSABLE_ENTITY (тип методов контроллеров вернуть обратно на void).
    • -
    • 2: Сделать валидацию принимаемых json объектов в REST контроллерах через ExceptionInfoHandler. Добавить в Rest контроллеры тест для невалидных данных. - -
    • -
    • 3: Сделать обработку ошибки при дублирования email ("User with this email already exists") для: -
        -
      • 3.1 регистрации / редактирования профиля пользователя - -
      • -
      • 3.2 добавления / редактирования пользователя в таблице
      • -
      • 3.3 REST контроллеров
      • -
      -
    • -
    -

    Optional

    - -
    -

    error Проверка в HW10

    -
      -
    • 1: ErrorInfo просто бин для передачи информации на клиента. Кода возврата и ответ настраиваются в ExceptionInfoHandler.
    • -
    • 2: Не дублируйте обработку ошибок BindingResult: result.getFieldErrors()..
    • -
    • 3: Можно не создавать собственные эксепшены, а в ExceptionInfoHandler ловить стандартные
    • -
    • 4: в MethodArgumentNotValidException также есть e.getBindingResult(), его можно обрабатывать по аналогии с BindException
    • -
    • 5: Не дублируйте код переключения локали на странице логина и в приложении
    • -
    • 6: При проблемах с валидацией Meals в MealRestController, посмотрите на валидацию в MealAjaxController.updateOrCreate
    • -
    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +``` +> - В `ExceptionInfoHandler.logAndGetErrorInfo()` также использую `ValidationUtil.getRootCause` + +- Сериализация Exception в JSON +- Exception Handling на уровне контроллера +- @ControllerAdvice improvements in Spring 4 +- @Valid @RequestBody + Error handling + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 8. Encoding password. Json READ/WRITE access +#### Apply 10_13_password_encoding.patch + +- Password Encoding + +#### Apply 10_14_read_write_access.patch +В реальном приложении для управления паролем необходим отдельный UI интерфейс с подтверждением старого пароля. + +- [@JsonProperty READ_ONLY / WRITE_ONLY](https://stackoverflow.com/a/12505165/548473) + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 9. Межсайтовая подделка запроса (CSRF). +#### Apply 10_15_csrf.patch +> Убрал `form:form` из ajax запросов: там csrf работает через header. Проверьте в вкладке браузера Network. + +**Поломалась UTF-8 кодировка в редактировании профиля и регистрациию (если по умолчанию не UTF-8). В Optional HW10 нужно будет починить.** + +- Межсайтовая подделка запроса (CSRF) +- Using Spring Security CSRF Protection +- Ajax and JSON Requests +- Stateless CSRF protection +- Ресурсы: + - Spring Security 4 + CSRF + - Are JSON web services vulnerable to CSRF attacks + - Правило ограничения домена (SOP) + - Cross-origin resource sharing (CORS) + +## ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Ваши вопросы + +> В чем отличие между аннотоацией `@PreAuthorize("hasRole('ROLE_ADMIN')")` и конфигурацией в jsp: ``, `` ? + +Анотация `@PreAuthorize` обрабатывается Spring анологично `@Transactional`, `@Cacheable` - класс проксируется и до-после вызова метода добавляется функциональность. +В данном случае перед вызовом метода проверяются роль залогиненного юзера. JSTL тэг `authorize` выполняет проверку условия в залогиненном юзере внутри jsp. + +> Еще раз: почему не нужен csrf для REST и нельзя подделать JSON запрос с вредоносного сайта? + +Попробуйте выполнить ajax запрос из вашего приложения c url, у которого домен отличный от вашего (например "http://topjava.herokuapp.com/meals/ajax/admin/users/"+id). +В консоли браузера будет `XMLHttpRequest cannot load`... - нарушение same origin policy. +Формам же разрешается делать submit (через `action=..`) на другой домен, но невозможно cделать `Content-Type`, отличный от стндартных enctype и методов кроме get и post. Таким образом `consumes = MediaType.APPLICATION_JSON_VALUE` в POST защищает приложение от CSRF. + +> Почему использован `BCryptPasswordEncoder`а не `hash(password+salt)`. Почему нельзя напрямую сравнивать закодированные пароли? + +[`BCryptPasswordEncoder` automatically generates a salt and concatenates it with the hash value in a single String](http://stackoverflow.com/a/8528804/548473). Для одного и тогоже пароля получаются разные хэши. Cравнивать их можно только через `isMatch()` с незахэшированным паролем. + +> Когда запускается в `GlobalControllerExceptionHandler` метод `defaultErrorHandler`? Когда как в него исключение попадает? Как выбирается, кто обрабатывает исключения: `ExceptionInfoHandler` или `GlobalControllerExceptionHandler`? + + `GlobalControllerExceptionHandler` попадает в контекст спринг (через `@ControllerAdvice` его находят в пакете `web`). Далее спринг перехватывает исключения и отправляет в подходящий по исключению метод `GlobalControllerExceptionHandler`. `ExceptionInfoHandler` помечен `@ControllerAdvice(annotations = RestController.class)`, он обрабатывает только ошибки из всех контроллеров с аннотацией `RestController.class`. + + > Откуда берутся в валидации сообщения на русском "должно быть между 10 и 10000"? + + Локализация встроена в Hibernate Validation. Смотрите `Ctrl+Shift+N` и `ValidationMessages_ru.properties`. + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание HW10 +- 1: Сделать валидацию в AdminAjaxController/MealAjaxController через `ExceptionInfoHandler`. Вернуть клиенту `ErrorInfo` и статус `HttpStatus.UNPROCESSABLE_ENTITY` (тип методов контроллеров вернуть обратно на `void`). +- 2: Сделать валидацию принимаемых json объектов в REST контроллерах через `ExceptionInfoHandler`. Добавить в Rest контроллеры тест для невалидных данных. + - @Valid @RequestBody + Error handling +- 3: Сделать обработку ошибки при дублирования email ("User with this email already exists") для: + - 3.1 регистрации / редактирования профиля пользователя + - Spring MVC form handling example + - 3.2 добавления / редактирования пользователя в таблице + - 3.3 REST контроллеров + +#### Optional +- 4: Сделать обработку ошибки при дублирования dateTime еды. Сделать тесты на дублирование email и dateTime. + - [Тесты на DB exception c @Transactional](http://stackoverflow.com/questions/37406714) + - [Сheck String in response body with mockMvc](https://stackoverflow.com/questions/18336277/how-to-check-string-in-response-body-with-mockmvc) +- 5: Сделать в приложении выбор локали (см. http://topjava.herokuapp.com/) + - Spring MVC internationalization sample + - Spring 4 MVC Internationalization +- 6: Починить UTF-8 в редактировании профиля и регистрации (если кодировка по умолчанию у вас не UTF-8). Подумайте, почему кодировка поломалась. + +------- + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Проверка в HW10 +- 1: `ErrorInfo` просто бин для передачи информации на клиента. Кода возврата и ответ настраиваются в `ExceptionInfoHandler`. +- 2: Не дублируйте обработку ошибок `BindingResult`: `result.getFieldErrors()..` +- 3: Можно не создавать собственные эксепшены, а в `ExceptionInfoHandler` ловить стандартные +- 4: в `MethodArgumentNotValidException` также есть `e.getBindingResult()`, его можно обрабатывать по аналогии с `BindException` +- 5: Не дублируйте код переключения локали на странице логина и в приложении +- 6: При проблемах с валидацией `Meals` в `MealRestController`, посмотрите на валидацию в `MealAjaxController.updateOrCreate` \ No newline at end of file diff --git a/doc/lesson11.md b/doc/lesson11.md index ba2e0092f619..e12d6828fc5c 100644 --- a/doc/lesson11.md +++ b/doc/lesson11.md @@ -1,1042 +1,203 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - topjava/lesson11.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - 104349b - - Jan 26, 2018 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    -
    - -
    - -
    - 204 lines (168 sloc) - - 21.3 KB -
    -
    - - -
    -

    Онлайн проекта Topjava

    -

    Обновил Демо приложение нашей версией

    -

    Материалы занятия

    -

    Некоторые темы урока новые, поэтому видео есть не везде.

    -

    Все материалы проекта (в том числе и будущие обновления) останутся доступны в Google Drive.

    -

    error Правка

    -

    Apply 11_0_fix.patch

    - -

    hw Разбор домашнего задания HW10

    -

    video 1. HW10

    -

    Apply 11_01_HW10_fix_encoding.patch

    -

    Apply 11_02_HW10_validation.patch

    -
    - -
    -

    Apply 11_03_HW10_duplicate_email.patch

    -
    -
      -
    • сделал код(ключ) i18n константой (EXCEPTION_DUPLICATE_EMAIL)
    • -
    -
    -

    Apply 11_04_HW10_duplicate_datetime.patch

    -
    -
      -
    • Реализавать обработку дублирования user.email и meal.dateTime можно по разному - -
    • -
    -
    -
    - -
    -

    question Вопрос:

    -
    -

    Поясните, почему без тесты на дубликаты падают без @Transactional(propagation = Propagation.NEVER)

    -
    -
      -
    • @Transactional в AbstractControllerTest означает, что коммита в базу не происходит, вместо него делается rollback. -А без сохранения в базу нарушение ограничения на дубликат не происходит.
    • -
    • @Transactional(propagation = Propagation.NEVER) над тестом отключает AbstractControllerTest @Transactional, в репозитории происходит сохранение в базу и в ExceptionInfoHandler ловим DataIntegrityViolationException.
    • -
    -

    video 2. HW10 Optional: change locale

    -

    Apply 11_05_HW10_change_locale.patch

    -
    -
      -
    • Вместо lang.jsp сделал общий bodyHeader.jsp
    • -
    • Добавил локализацию календаря $.datetimepicker.setLocale(localeCode)
    • -
    • Вместо смену локали в lang.jsp через javascript сделал href=${requestScope['javax.servlet.forward.request_uri']}?lang=..
    • -
    -
    -

    Bootstrap Navbar

    -

    Заключительное 11-е занятие

    -

    Локализация:

    -

    Apply 11_06_i18n.patch

    -
      -
    • Вынес общие части опций таблиц в datatablesUtil.js используя jQuery.extend()
    • -
    • Добавил локализацию Search в datatable
    • -
    • Сделал локализацию ошибок валидации: -
        -
      • Все коды для локализации конкретной ошибки валидации можно посмотреть в дебаге: FieldError.getCodes()
      • -
      • Коды для ее аргументов: FieldError.getArguments() (аргумент складывается из имени класса и поля).
      • -
      -
    • -
    • Добавил локализацию ErrorInfo.type (код локализации в ErrorType и поле ErrorInfo.typeMessage)
    • -
    -

    Защита от XSS (Cross Site Scripting)

    -
    -

    Попробуйте до и после патча ввести в любое текстовое поле редактирования <script>alert('XSS')</script> и сохранить.

    -
    -

    Apply 11_07_XSS.patch

    -
    -
      -
    • password проверять не надо, т.к. он не выводится в html, а email надо
    • -
    • Сделал общий интерфейс валидации View.Web и @Validated(View.Web.class) вместо @Valid для проверки содержимого только на входе UI/REST. При сохранении проверка на безопасный html контент не делается.
    • -
    -
    - -

    Обработка ошибок 404 (NotFound)

    -

    Apply 11_08_404.patch

    -

    Доступ к AuthorizedUser

    -

    Apply 11_09_auth_user.patch

    -
      -
    • Автоподстановка в контроллерах -
        -
      • не стал делать автоподстановку по всем контроллерам (в абстрактных контроллерах проще работать с AuthorizedUser, чем получать его через @AuthenticationPrincipal и передавать параметром)
      • -
      -
    • -
    • В JSP: the authentication Tag -
        -
      • авторизованный пользователь доступен в JSP через tag authentication, интерсептор становится не нужным
      • -
      -
    • -
    -

    Ограничение модификации пользователей

    -

    Apply 11_10_restrict_modification.patch

    -
      -
    • В AbstractUserController добавилась защита от удаления-модификации Admin/User для профиля HEROKUAbstractUserController заинжектил Environment и сделал проверку HEROKU)
    • -
    • Чтобы тесты были рабочими, ввел профиль HEROKU, работающий так же, как и POSTGRES.
    • -
    • Добавил ApplicationException для работы с ошибками с поддержкой i18n в приложении (от него отнаследовал NotFoundException и ModificationRestrictionException)
    • -
    -

    video 3. Деплой приложения в Heroku.

    -

    Apply 11_11_heroku.patch

    -
    - -
    -

    Приложение деплоится в ROOT: http://localhost:8080

    - -
    Config Vars
    +# Онлайн проекта Topjava
    +
    +### Обновил Демо приложение нашей версией
    +
    +## Материалы занятия
    +#### Некоторые темы урока новые, поэтому видео есть не везде.
    +#### Все материалы проекта (в том числе и будущие обновления) останутся доступны в Google Drive.
    +
    +### ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Правка
    +
    +#### Apply 11_0_fix.patch
    +- Поправил `app_ru.properties`
    +  - Spring в отличии от JSP может работать с UTF-8.
    +  - Проверьте в настройках IDEA [справа внизу выключенный флаг _Transparent native-to-ascii conversion_](https://github.com/JavaOPs/topjava/wiki/IDEA#%D0%9F%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C-%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D1%83-utf-8)
    +- Поправил [сodacy issues](https://www.codacy.com/app/javawebinar/topjava/issues/index?bid=5019773&filters=W3siaWQiOiJDYXRlZ29yeSIsInZhbHVlcyI6WyJFcnJvciBQcm9uZSJdfV0=)
    +  - В `exception.jsp` подразумевается оставить закомментированный код HTML на странице, потому используется ``
    +
    +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Разбор домашнего задания HW10
    +
    +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 1. HW10
    +#### Apply 11_01_HW10_fix_encoding.patch
    +#### Apply 11_02_HW10_validation.patch
    +> - В [соответствии со спицификацией](http://stackoverflow.com/a/22358422/548473) для поменял `HTTP 400` (ошибка в структуре сообщения) на `HTTP 422` (ошибка в содержании)
    +> - Сделал тесты и проверку типа ошибки [через jsonPath](https://www.petrikainulainen.net/programming/spring-framework/integration-testing-of-spring-mvc-applications-write-clean-assertions-with-jsonpath/)
    +
    +#### Apply 11_03_HW10_duplicate_email.patch
    +> - сделал код(ключ) i18n константой (`EXCEPTION_DUPLICATE_EMAIL`)
    +
    +#### Apply 11_04_HW10_duplicate_datetime.patch
    +> - Реализавать обработку дублирования `user.email` и `meal.dateTime` можно по разному
    +>   - через [поиск в сообщении `DataIntegrityViolationException` имени DB constrains](https://stackoverflow.com/a/42422568/548473)
    +>   - через [Controller Based Exception Handling](https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc#controller-based-exception-handling)
    +>   - через [Custom Validator](https://howtodoinjava.com/spring/spring-mvc/spring-mvc-custom-validator-example/), с еще одним запросом в базу. Первый самый простой и расширяемый (хотя зависить от базы), выбрал его. Для работы с HSQLDB сделал `toLowerCase`. Самый корректный и трудоемкий с собственными валидаторами: он позволяет валидировать в конроллерах, а не при сохранении.
    +
    +> - Сделал утильный класс `MessageUtil` для работы с i18n.
    +> - Добавил тесты на дублирование. Отключил транзакционность в тестах на дублирование через `@Transactional(propagation = Propagation.NEVER)`.
    +>   - [Решение проблемы с транзакционными тестами](https://stackoverflow.com/a/46415060/548473)
    +
    +#### ![question](https://cloud.githubusercontent.com/assets/13649199/13672858/9cd58692-e6e7-11e5-905d-c295d2a456f1.png) Вопрос:
    +> Поясните, почему без тесты на дубликаты падают без `@Transactional(propagation = Propagation.NEVER)`
    +
    +- `@Transactional` в `AbstractControllerTest` означает, что коммита в базу не происходит, вместо него делается rollback.
    +А без сохранения в базу нарушение ограничения на дубликат не происходит. 
    +- `@Transactional(propagation = Propagation.NEVER)` над тестом  отключает `AbstractControllerTest @Transactional`, в репозитории происходит сохранение в базу и в `ExceptionInfoHandler` ловим `DataIntegrityViolationException`.
    +
    +###  ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 2. HW10 Optional: change locale
    +#### Apply 11_05_HW10_change_locale.patch
    +> - Вместо `lang.jsp` сделал общий `bodyHeader.jsp`
    +> - Добавил локализацию календаря `$.datetimepicker.setLocale(localeCode)`
    +> - Вместо смену локали в `lang.jsp` через javascript сделал `href=${requestScope['javax.servlet.forward.request_uri']}?lang=..`
    +
    +Bootstrap Navbar
    +
    +## Заключительное 11-е занятие
    +
    +### Локализация:
    +#### Apply 11_06_i18n.patch
    + - Вынес общие части опций таблиц в `datatablesUtil.js` используя jQuery.extend()
    + - Добавил локализацию Search в datatable
    + - Сделал локализацию ошибок валидации:
    +   - Все коды для локализации конкретной ошибки валидации можно посмотреть в дебаге: `FieldError.getCodes()`
    +   - Коды для ее аргументов: `FieldError.getArguments()` (аргумент складывается из имени класса и поля).
    + - Добавил локализацию `ErrorInfo.type` (код локализации в `ErrorType` и поле `ErrorInfo.typeMessage`)
    +
    +### Защита от XSS (Cross Site Scripting)
    +> **Попробуйте до и после патча ввести в любое текстовое поле редактирования `` и сохранить.**
    +#### Apply 11_07_XSS.patch
    +> - `password` проверять не надо, т.к. он не выводится в html, а [email надо](https://stackoverflow.com/questions/17480809)
    +> - Сделал общий интерфейс валидации `View.Web` и `@Validated(View.Web.class)` вместо `@Valid` для проверки содержимого только на входе UI/REST. При сохранении проверка на безопасный html контент не делается.
    +
    +- XSS для новичков
    +- XSS глазами злоумышленника
    +- Prevent people from doing XSS in Spring MVC
    +
    +### [Обработка ошибок 404 (NotFound)](https://stackoverflow.com/questions/18322279/spring-mvc-spring-security-and-error-handling)
    +#### Apply 11_08_404.patch
    +
    +### Доступ к AuthorizedUser
    +#### Apply 11_09_auth_user.patch
    +- [Автоподстановка в контроллерах](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-authentication-principal)
    +  - не стал делать автоподстановку по всем контроллерам (в абстрактных контроллерах проще работать с `AuthorizedUser`, чем получать его через `@AuthenticationPrincipal` и передавать параметром)
    +- [В JSP: the authentication Tag](https://docs.spring.io/spring-security/site/docs/current/reference/html/taglibs.html#the-authentication-tag)
    +  - авторизованный пользователь доступен в JSP через tag `authentication`, интерсептор становится не нужным
    +
    +### Ограничение модификации пользователей
    +#### Apply 11_10_restrict_modification.patch
    + - В `AbstractUserController` добавилась защита от удаления-модификации `Admin/User` для профиля `HEROKU` (в `AbstractUserController` заинжектил `Environment` и сделал проверку `HEROKU`)
    + - Чтобы тесты были рабочими, ввел профиль `HEROKU`, работающий так же, как и `POSTGRES`.
    + - Добавил `ApplicationException` для работы с ошибками с поддержкой i18n в приложении (от него отнаследовал `NotFoundException` и `ModificationRestrictionException`)
    +
    +###  ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png)  3. Деплой приложения в Heroku.
    +#### Apply 11_11_heroku.patch
    +
    +> - Добавил зависимости `postgres` в профиль мавена `heroku`
    +> - [Поменял настройки `dataSource` для профиля `heroku`](http://stackoverflow.com/questions/10684244/dbcp-validationquery-for-different-databases). 
    +При опускании/поднятии приложения в heroku.com портятся коннекты в пуле и необходимо их валидировать. 
    +
    +**Приложение деплоится в ROOT: [http://localhost:8080](http://localhost:8080)**
    +
    +- Деплой Java Spring приложения в PaaS-платформу Heroku
    +```
    +Config Vars
       ERROR_PAGE_URL=...
       TOPJAVA_ROOT=/app
     
     Datasources advanced
         ssl=true
         sslfactory=org.postgresql.ssl.NonValidatingFactory
    -
    - -

    Apply 11_12_fix_locale.patch

    -
      -
    • Починка тестов, зависящих от локали окружения
    • -
    -

    video 4. Собеседование. Разработка ПО

    - -

    hw Домашнее задание по проекту:

    -
      -
    • Задеплоить свое приложение в Heroku
    • -
    • Сделать / обновить резюме (отдать на ревью в канал #hw11 группы slack) - -
    • -
    • Заполнить в своем профиле Java Online Projects ссылку на резюме и информацию по поиску работы (если конечно актуально): резюме, флаги рассматриваю работу, готов к релокации и информация для HR.
    • -
    • Обновления базы соискателей буду рассылать по HR ночью на 31.01, постарайтесь успеть
    • -
    • Сделать и внести в свое портфолио Graduation project -
        -
      • Для участников с проверкой выпускной проект принимаю на ревью до 15.02 (участникам Masterjava до 31.05).
      • -
      -
    • -
    • Пройти важную тему МНОГОПОТОЧНОСТЬ по первому уроку Masterjava. -
        -
      • Обучение на Masterjava сейчас идет в индивидуальном режиме, старт в любое время.
      • -
      • Проект, патчи, группа Slack, занятия и видео анологичны проекту Topjava.
      • -
      • Цена для участников Topjava снижена
      • -
      -
    • -
    -

    Возможные доработки приложения:

    -
      -
    • Для редактирования паролей сделать отдельный интерфейс с запросом старого пароля и кнопку сброса пароля для администратора.
    • -
    • Добавление и удаление ролей для пользователей в админке.
    • -
    • Перевести UI на Angular / Vaadin elements /GWT /GXT /Vaadin / ZK/ [Ваш любимый фреймворк]..
    • -
    • Перевести проект на Spring Boot
    • -
    • Перевести шаблоны с JSP на Thymeleaf
    • -
    • Сделать авторизацию в приложение по OAuth 2.0 (Spring Security OAuth, -VK auth, github oauth, ...)
    • -
    • Сделать подтверждение регистрации пользователя по email c возможностью восстановления пароля.
    • -
    • Сделать отображение еды постранично, с поиском и сортировкой на стороне сервера.
    • -
    • Перевод проекта на https
    • -
    • Сделать desktop/mobile приложение, работающее по REST с нашим приложением.
    • -
    • Показ ошибок в модальном окне редактирования таблицы так же, как и в JSP профиля
    • -
    • Limit login attempts example
    • -
    • Сделать авторизацию REST по JWT
    • -
    -

    Доработки участников прошлых выпусков:

    - -

    Жду твою доработку из списка!

    -

    Ресурсы по Проекту

    - -

    Пожалуйста ответь на 4-6 вопросов по проекту, это важно!

    -

    error Замечания по резюме:

    -
      -
    • варианты размещения: google doc + linkedin + hh + ...
    • -
    • хорошо, если будет в html формате (например в https://pages.github.com/). Например так, на github
    • -
    • возраст и день рождения можно не писать
    • -
    • все упоминания Junior убрать!
    • -
    • линки делай кликабельными (если формат поддерживает)
    • -
    • если нет опыта в IT, обязательно вставь участие в стажировке Topjava. Также вставь ссылку на свой сертификат (она есть в личном профиле). Весь не-IT опыт можно кратко.
    • -
    • всю выгодную для себя информацию (и важную для HR) распологайте вверху. Название секций в резюме и их порядок относительно стандартный и важный
    • -
    -
    -
    - -
    - - - - - -
    - -
    - -
    -
    - -
    - - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - - - - - - +``` + +- Ресурсы: + - PaaS-платформа Heroku + - Конфигурирование приложения для запуска через Tomcat-based Java Web + - Конфигурирование DataSource profile для Heroku + - Интерграция с IDEA + - Find your Platform as a Service + - Getting Started with Java on Heroku + - Managing Your SSH Keys + - Deploy your application to Heroku + - Развертывание приложений Java с помощью PaaS от Heroku + - A Java Developer’s Guide to PaaS + - A Simple PaaS Comparison Guide (With the Java Dev in Mind) + - Java PaaS shootout + +#### Apply 11_12_fix_locale.patch +- Починка тестов, зависящих от локали окружения + +### ![video](https://cloud.githubusercontent.com/assets/13649199/13672715/06dbc6ce-e6e7-11e5-81a9-04fbddb9e488.png) 4. Собеседование. Разработка ПО +- [Темы/ресурсы тестового собеседования](http://javaops.ru/interview/test.html) +- [Составление резюме, подготовка к интервью, поиск работы](https://github.com/JavaOPs/topjava/blob/master/cv.md) +- [Слайды](https://docs.google.com/presentation/d/18o__IGRqYadi4jx2wX2rX6AChHh-KrxktD8xI7bS33k), [Книги](http://javawebinar.ru/books.html) +- [Jenkins/Hudson: что такое и для чего он нужен](https://habrahabr.ru/post/334730/) + +## ![hw](https://cloud.githubusercontent.com/assets/13649199/13672719/09593080-e6e7-11e5-81d1-5cb629c438ca.png) Домашнее задание по проекту: +- **Задеплоить свое приложение в Heroku** +- Сделать / обновить резюме (отдать на ревью в канал #hw11 группы slack) + - [Resume Templates](http://uptowork.com/resume-templates) + - Выбираете по вкусу, заходите в корзину покупок и в правом нижнем углу "I have a discount code" вводите: **ZgyI50xW**. Компания поделилась со мной кодом в замен на ваши отзывы об использовании сервиса. + - [Загрузка сайта на GitHub. Бесплатный хостинг и домен.](https://vk.com/video-58538268_456239051?list=661b165047264e7952) + - [CSS theme for hosting your personal site, blog, or portfolio](https://mademistakes.com/work/minimal-mistakes-jekyll-theme/) +- Заполнить в [своем профиле Java Online Projects](http://javaops.ru/view/login) ссылку на резюме и информацию по поиску работы (если конечно актуально): резюме, флаги рассматриваю работу, готов к релокации и информация для HR. +- **Обновления базы соискателей буду рассылать по HR ночью на 31.01, постарайтесь успеть** +- Сделать и внести в свое портфолио Graduation project + - Для участников с проверкой выпускной проект принимаю на ревью до 15.02 (участникам Masterjava до 31.05). +- **Пройти важную тему МНОГОПОТОЧНОСТЬ по первому уроку Masterjava.** + - Обучение на Masterjava сейчас идет в индивидуальном режиме, старт в любое время. + - Проект, патчи, группа Slack, занятия и видео анологичны проекту Topjava. + - **Цена для участников Topjava снижена** + +## Возможные доработки приложения: +- Для редактирования паролей сделать отдельный интерфейс с запросом старого пароля и кнопку сброса пароля для администратора. +- Добавление и удаление ролей для пользователей в админке. +- Перевести UI на Angular / Vaadin elements /GWT /GXT /Vaadin / ZK/ [Ваш любимый фреймворк].. +- Перевести проект на Spring Boot +- Перевести шаблоны с JSP на Thymeleaf +- Сделать авторизацию в приложение по OAuth 2.0 (Spring Security OAuth, +VK auth, github oauth, ...) +- Сделать подтверждение регистрации пользователя по email c возможностью восстановления пароля. +- Сделать отображение еды постранично, с поиском и сортировкой на стороне сервера. +- Перевод проекта на https +- Сделать desktop/mobile приложение, работающее по REST с нашим приложением. +- Показ ошибок в модальном окне редактирования таблицы так же, как и в JSP профиля +- Limit login attempts example +- Сделать авторизацию REST по JWT + +## Доработки участников прошлых выпусков: +- [Авторизация в приложение по OAuth2 через GitHub](http://rblik-topjava.herokuapp.com) + - [GitHub, ветка oauth](https://github.com/rblik/topjava/tree/oauth) +- [Авторизация в приложение по OAuth2 через GitHub/Facebook/Google](http://tj9.herokuapp.com) + - [GitHub](https://github.com/jacksn/topjava) +- [Angular 2 UI](https://topjava-angular2.herokuapp.com) + - [tutorial по доработке](https://github.com/Gwulior/article/blob/master/article.md) + - [ветка angular2 в гитхабе](https://github.com/12ozCode/topjava08-to-angular2/tree/angular2) +- [Отдельный фронтэнд на Angular 2, который работает по REST с авторизацией по JWT](https://topjava6-frontend.herokuapp.com) + - [ветка development фронтэнда](https://github.com/evgeniycheban/topjava-frontend/tree/development) + - [ветка development бэкэнда](https://github.com/evgeniycheban/topjava/tree/development) + - в JWT токенен приложение topjava передает email, name и роль admin как boolean true/false, +на клиенте он декодируется и из него получается auth-user, с которым уже работает фронтэнд + +#### Жду твою доработку из списка! + +### Ресурсы по Проекту +- Уроки Bootstrap 3 +- Spring at tutorialspoint +- Articles in Spring +- Learn Spring on Baeldung +- Spring Framework + Reference Documentation +- Hibernate Reference Documentation +- Java Course (книга 2) +- Справочник «Паттерны проектирования» +- Catalog of Patterns of Enterprise Application Architecture + +## Пожалуйста ответь на 4-6 вопросов по проекту, это важно! + +## ![error](https://cloud.githubusercontent.com/assets/13649199/13672935/ef09ec1e-e6e7-11e5-9f79-d1641c05cbe6.png) Замечания по резюме: +- варианты размещения: google doc + linkedin + hh + ... +- хорошо, если будет в html формате (например в https://pages.github.com/). [Например так](https://gkislin.github.io/), [на github](https://github.com/gkislin/gkislin.github.io/blob/master/index.html) +- возраст и день рождения можно не писать +- все упоминания Junior убрать! +- линки делай кликабельными (если формат поддерживает) +- если нет опыта в IT, обязательно вставь [участие в стажировке Topjava](https://github.com/JavaOPs/topjava/blob/master/cv.md#Позиционирование-проекта-topjava). Также вставь ссылку на свой сертификат (она есть в личном профиле). Весь не-IT опыт можно кратко. +- всю выгодную для себя информацию (и важную для HR) распологайте вверху. Название секций в резюме и их порядок относительно стандартный и важный \ No newline at end of file diff --git a/doc/meals_index.md b/doc/meals_index.md index 90146402c543..53fdb631239f 100644 --- a/doc/meals_index.md +++ b/doc/meals_index.md @@ -1,671 +1,10 @@ +## Запросы к таблице meals c разным index (1000 пользователей, у каждого за год по 3 записи в день): +### Проверьте у себя, цифры у всех разные! - - - - - - - - - - - - - - - - - - - - - - - - - - - - topjava/meals_index.md at doc · JavaWebinar/topjava - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Skip to content -
    - - - - - - - - - - - - - -
    - -
    - -
    -
    - - - -
    -
    -
    - - - - - - -
    -
    - -
      -
    • -
      - -
      - - - -
      -
      -
      - - Notifications -
      - - - -
      -
      -
      -
      -
    • - -
    • - -
      -
      - - - -
      -
      - - - -
      - -
    • - -
    • -
      - -
      - -
    • -
    - -

    - - /topjava - -

    - -
    - - - - -
    - -
    -
    - - - Permalink - - - -
    - -
    - - -
    - -
    -
    - - Switch branches/tags -
    - -
    -
    - -
    -
    - -
    -
    - - - -
    -
    - - -
    - -
    Nothing to show
    -
    - -
    -
    -
    - -
    - - Find file - - -
    - -
    - - - -
    - - - 5b5c8c6 - - Nov 23, 2017 - - - -
    - - -
    - - -
    - - -
    -
    -
    - -
    - Raw - Blame - History -
    - - - - - -
    - -
    - -
    - -
    - 145 lines (119 sloc) - - 5.12 KB -
    -
    - - -
    -

    Запросы к таблице meals c разным index (1000 пользователей, у каждого за год по 3 записи в день):

    -

    Проверьте у себя, цифры у всех разные!

    -

    Скрипт по наполнению базы на PL/pgSQL:

    -
    drop function if exists populate_db();
    +### Скрипт по наполнению базы на PL/pgSQL:
    +```
    +drop function if exists populate_db();
     
     create function populate_db() returns void as $$
     declare
    @@ -698,190 +37,108 @@ end;
     $$ LANGUAGE plpgsql;
     
     select populate_db();
    -
    -

    Без индекса

    -

    время выполнения 200ms/150ms

    -
    -

    DROP INDEX IF EXISTS meals_unique_user_datetime_idx;

    -
    -
    -

    EXPLAIN ANALYZE -SELECT * FROM meals -WHERE user_id = 100000 AND date_time BETWEEN '2015-02-10' AND '2015-05-20' -ORDER BY date_time DESC;

    -
    -
    Sort  (cost=28292.17..28292.88 rows=286 width=31) (actual time=203.774..203.782 rows=298 loops=1)
    +```
    +### Без индекса
    +**время выполнения 200ms/150ms**
    +
    +> DROP INDEX IF EXISTS meals_unique_user_datetime_idx;
    +
    +> EXPLAIN ANALYZE
    +         SELECT * FROM meals
    +          WHERE user_id = 100000 AND date_time BETWEEN '2015-02-10' AND '2015-05-20'
    +          ORDER BY date_time DESC;
    +          
    +```
    +Sort  (cost=28292.17..28292.88 rows=286 width=31) (actual time=203.774..203.782 rows=298 loops=1)
       Sort Key: date_time DESC
       Sort Method: quicksort  Memory: 48kB
    -  ->  Seq Scan on meals  (cost=0.00..28280.50 rows=286 width=31) (actual time=0.023..203.714 rows=298 loops=1)
    -        Filter: ((date_time >= '2015-02-10 00:00:00'::timestamp without time zone) AND (date_time <= '2015-05-20 00:00:00'::timestamp without time zone) AND (user_id = 100000))
    +  ->  Seq Scan on meals  (cost=0.00..28280.50 rows=286 width=31) (actual time=0.023..203.714 rows=298 loops=1)
    +        Filter: ((date_time >= '2015-02-10 00:00:00'::timestamp without time zone) AND (date_time <= '2015-05-20 00:00:00'::timestamp without time zone) AND (user_id = 100000))
             Rows Removed by Filter: 1094702
     Planning time: 0.102 ms
     Execution time: 203.822 ms
    -
    -
    -

    EXPLAIN ANALYZE -SELECT * FROM meals -WHERE user_id = 100003 -ORDER BY date_time DESC;

    -
    -
    Sort  (cost=22858.59..22861.23 rows=1057 width=31) (actual time=147.481..147.481 rows=0 loops=1)
    +```
    +
    +> EXPLAIN ANALYZE
    +         SELECT * FROM meals
    +          WHERE user_id = 100003
    +          ORDER BY date_time DESC;
    +
    +```
    +Sort  (cost=22858.59..22861.23 rows=1057 width=31) (actual time=147.481..147.481 rows=0 loops=1)
       Sort Key: date_time DESC
       Sort Method: quicksort  Memory: 25kB
    -  ->  Seq Scan on meals  (cost=0.00..22805.50 rows=1057 width=31) (actual time=147.474..147.474 rows=0 loops=1)
    +  ->  Seq Scan on meals  (cost=0.00..22805.50 rows=1057 width=31) (actual time=147.474..147.474 rows=0 loops=1)
             Filter: (user_id = 100003)
             Rows Removed by Filter: 1095000
     Planning time: 0.097 ms
     Execution time: 147.514 ms
    -
    -

    Индекс по user_id;

    -

    время выполнения 0.415ms/0.05ms

    -
    -

    CREATE INDEX meals_idx ON meals (user_id);

    -
    -
    -

    EXPLAIN ANALYZE -SELECT * FROM meals -WHERE user_id = 100000 AND date_time BETWEEN '2015-02-10' AND '2015-05-20' -ORDER BY date_time DESC;

    -
    -
    Sort  (cost=59.88..60.59 rows=286 width=31) (actual time=0.372..0.378 rows=298 loops=1)
    +```
    +
    +### Индекс по user_id;
    +**время выполнения 0.415ms/0.05ms**
    +
    +> CREATE INDEX meals_idx ON meals (user_id);
    +
    +> EXPLAIN ANALYZE
    +         SELECT * FROM meals
    +          WHERE user_id = 100000 AND date_time BETWEEN '2015-02-10' AND '2015-05-20'
    +          ORDER BY date_time DESC;
    +
    +```
    +Sort  (cost=59.88..60.59 rows=286 width=31) (actual time=0.372..0.378 rows=298 loops=1)
       Sort Key: date_time DESC
       Sort Method: quicksort  Memory: 48kB
    -  ->  Index Scan using meals_user_id_idx on meals  (cost=0.43..48.21 rows=286 width=31) (actual time=0.048..0.307 rows=298 loops=1)
    +  ->  Index Scan using meals_user_id_idx on meals  (cost=0.43..48.21 rows=286 width=31) (actual time=0.048..0.307 rows=298 loops=1)
             Index Cond: (user_id = 100000)
    -        Filter: ((date_time >= '2015-02-10 00:00:00'::timestamp without time zone) AND (date_time <= '2015-05-20 00:00:00'::timestamp without time zone))
    +        Filter: ((date_time >= '2015-02-10 00:00:00'::timestamp without time zone) AND (date_time <= '2015-05-20 00:00:00'::timestamp without time zone))
             Rows Removed by Filter: 797
     Planning time: 0.123 ms
     Execution time: 0.414 ms
    -
    -
    -

    EXPLAIN ANALYZE -SELECT * FROM meals -WHERE user_id = 100003 -ORDER BY date_time DESC;

    -
    -
    Sort  (cost=96.02..98.66 rows=1057 width=31) (actual time=0.020..0.020 rows=0 loops=1)
    +```
    +
    +> EXPLAIN ANALYZE
    +         SELECT * FROM meals
    +          WHERE user_id = 100003
    +          ORDER BY date_time DESC;
    +
    +```
    +Sort  (cost=96.02..98.66 rows=1057 width=31) (actual time=0.020..0.020 rows=0 loops=1)
       Sort Key: date_time DESC
       Sort Method: quicksort  Memory: 25kB
    -  ->  Index Scan using meals_user_id_idx on meals  (cost=0.43..42.93 rows=1057 width=31) (actual time=0.013..0.013 rows=0 loops=1)
    +  ->  Index Scan using meals_user_id_idx on meals  (cost=0.43..42.93 rows=1057 width=31) (actual time=0.013..0.013 rows=0 loops=1)
             Index Cond: (user_id = 100003)
     Planning time: 0.095 ms
     Execution time: 0.047 ms
    -
    -

    Составной индекс по user_id и date_time (ЗАВИСИТ ОТ ПОРЯДКА ЗАДАНИЯ КОЛОНОК!)

    -

    время выполнения 0.110ms/0.035ms

    -
    -

    DROP INDEX meals_idx;

    -
    -
    -

    CREATE INDEX meals_idx ON meals (user_id, date_time);

    -
    -
    -

    EXPLAIN ANALYZE -SELECT * FROM meals -WHERE user_id = 100000 AND date_time BETWEEN '2015-02-10' AND '2015-05-20' -ORDER BY date_time DESC;

    -
    -
    Index Scan Backward using meals_user_id_date_time_idx on meals  (cost=0.43..511.74 rows=286 width=31) (actual time=0.026..0.081 rows=298 loops=1)
    -  Index Cond: ((user_id = 100000) AND (date_time >= '2015-02-10 00:00:00'::timestamp without time zone) AND (date_time <= '2015-05-20 00:00:00'::timestamp without time zone))
    -Planning time: 0.130 ms
    -Execution time: 0.112 ms
    -
    -
    -

    EXPLAIN ANALYZE -SELECT * FROM meals -WHERE user_id = 100003 -ORDER BY date_time DESC;

    -
    -
    Index Scan Backward using meals_user_id_date_time_idx on meals  (cost=0.43..1795.68 rows=1057 width=31) (actual time=0.012..0.012 rows=0 loops=1)
    -  Index Cond: (user_id = 100003)
    -Planning time: 0.103 ms
    -Execution time: 0.033 ms
    -
    -
    -
    - -
    - - - - -
    - -
    - -
    -
    +``` -
    +### Составной индекс по user_id и date_time (ЗАВИСИТ ОТ ПОРЯДКА ЗАДАНИЯ КОЛОНОК!) +**время выполнения 0.110ms/0.035ms** - - - - - -
    - - - You can't perform that action at this time. -
    - - - - - - - - - - -
    - - You signed in with another tab or window. Reload to refresh your session. - You signed out in another tab or window. Reload to refresh your session. -
    - +> EXPLAIN ANALYZE + SELECT * FROM meals + WHERE user_id = 100000 AND date_time BETWEEN '2015-02-10' AND '2015-05-20' + ORDER BY date_time DESC; +``` +Index Scan Backward using meals_user_id_date_time_idx on meals (cost=0.43..511.74 rows=286 width=31) (actual time=0.026..0.081 rows=298 loops=1) + Index Cond: ((user_id = 100000) AND (date_time >= '2015-02-10 00:00:00'::timestamp without time zone) AND (date_time <= '2015-05-20 00:00:00'::timestamp without time zone)) +Planning time: 0.130 ms +Execution time: 0.112 ms +``` - - +> EXPLAIN ANALYZE + SELECT * FROM meals + WHERE user_id = 100003 + ORDER BY date_time DESC; +``` +Index Scan Backward using meals_user_id_date_time_idx on meals (cost=0.43..1795.68 rows=1057 width=31) (actual time=0.012..0.012 rows=0 loops=1) + Index Cond: (user_id = 100003) +Planning time: 0.103 ms +Execution time: 0.033 ms +``` \ No newline at end of file