diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000000..68ab0a037ad1
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,22 @@
+# https://docs.travis-ci.com/user/languages/java/
+language: java
+jdk: oraclejdk8
+
+#https://dzone.com/articles/travis-ci-tutorial-java-projects
+cache:
+ directories:
+ - $HOME/.m2
+
+# https://docs.travis-ci.com/user/database-setup/#PostgreSQL
+before_script:
+- psql -c 'create database topjava' -U postgres
+- psql -c 'create user "user"; grant all privileges on database topjava to "user"' -U postgres
+
+# https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches
+branches:
+ only:
+ - master
+
+# https://docs.travis-ci.com/user/notifications#Configuring-email-notifications
+#notifications:
+# email: false
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 000000000000..4183a09d949b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,198 @@
+Java Enterprise Online Project
+===============================
+Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), datatables, jQuery + plugins, Java 8 Stream and Time API и хранением в базах данных Postgresql и HSQLDB.
+
+
+
+ Когда вы слышите что-то, вы забываете это.
+ Когда вы видите что-то, вы запоминаете это.
+ Но только когда вы начинаете делать это,
+ вы начинаете понимать это
+
+ Старинная китайская поговорка
+
+## Описание и план проекта
+### Демо разрабатываемого приложения
+### [Изменения проекта (Release Notes)](ReleaseNotes.md)
+### Требования к участникам, Wiki
+### Составление резюме, подготовка к интервью, поиск работы
+
+Вводное занятие (обязательно смотреть все видео)
+===============
+##  1. Осваиваем Java Enterprise. Трудоустройство. Ответы на вопросы.
+- Слайды презентации
+- Java Tools and Technologies Landscape Report 2016
+- [Java in 2017 Survey](http://www.baeldung.com/java-in-2017)
+- Из юниоров в разработчики: получаем первую работу
+
+#### Spring Pet-Clinic
+- Spring PetClinic Sample Application
+- Presentation
+
+##  2. Системы управления версиями. Git.
+- **Wiki по ведению проекта в Git**
+- Система управления версиями. VCS/DVSC.
+- Ресурсы:
+ - Интерактивная Git обучалка
+ - Еще одна интерактивная обучалка, по-русски
+ - Книга Git
+ - Working with remote repositories
+ - Видео по обучению Git
+ - Git Overview
+ - [Основы Git за 20 минут](https://www.youtube.com/watch?v=TMeZGvtQnT8)
+ - [Git - для новичков](https://www.youtube.com/watch?list=PLY4rE9dstrJyTdVJpv7FibSaXB4BHPInb&v=PEKN8NtBDQ0)
+
+##  3. Работа с проектом (выполнять инструкции)
+**ВНИМАНИЕ: выбирайте для проекта простой пусть без пробелов и русских букв, например (Windows) `c:\projects\topjava\`. Иначе впоследствии будут проблемы**
+> Проект постоянно улучшается, поэтому видео иногда отличается от кода проекта. Изменения указываю после видео: в `UserMeals/UserMealWithExceed` поля изменились на `private`
+- **Prepare_ to_ HW0.patch (скачать и положить в каталог вашего проекта)**
+
+## Инструкция по шагам (из видео):
+- Установить ПО (git, JDK8, IntelliJ IDEA, Maven)
+- Создать аккаунт на GitHub
+- Сделать Fork **ЭТОГО** проекта (https://github.com/JavaOPs/topjava)
+- Сделать локальный репозиторий проекта:
+
+- Открыть и настроить проект в IDEA
+ - Выставить кодировку UTF-8 в консоли
+ - Поставить кодировку UTF-8
+ - Поменять фонт по умолчанию (DejaVu)
+- По ходу видео сделать Apply Patch... скаченного патча Prepare_ to_ HW0.patch
+- Закоммитить и запушить изменения (commit + push)
+- Сделать ветку домашнего задания
+- Выполнить задание и залить на GitHub (commit + push)
+- Переключиться в основную ветку проекта master.
+
+##  Домашнее задание HW0
+```
+Реализовать метод UserMealsUtil.getFilteredWithExceeded:
+- должны возвращаться только записи между startTime и endTime
+- поле UserMealWithExceed.exceed должно показывать,
+ превышает ли сумма калорий за весь день параметра метода caloriesPerDay
+
+Т.е UserMealWithExceed - это запись одной еды, но поле exceeded будет одинаково для всех записей за этот день.
+
+- Проверьте результат выполнения ДЗ (можно проверить логику в http://topjava.herokuapp.com , список еды)
+- Оцените Time complexity вашего алгоритма, если он O(N*N)- попробуйте сделать O(N).
+```
+- Java 8 Date and Time API
+- Алгоритмы и структуры данных для начинающих: сложность алгоритмов
+- Time complexity
+- Временная сложность алгоритма
+- Вычислительная сложность
+
+#### Optional (Java 8 Stream API)
+```
+Сделать реализацию через Java 8 Stream API.
+```
+- Видео: Доступно о Java 8 Lambda
+- Java 8: Lambda выражения
+- Java 8: Потоки
+- Pуководство по Java 8 Stream
+- [7 способов использовать groupingBy в Stream API](https://habrahabr.ru/post/348536)
+- Лямбда-выражения в Java 8
+- A Guide to Java 8
+- Шпаргалка Java Stream API
+- Алексея Владыкин: Элементы функционального программирования в Java
+- Yakov Fain о новом в Java 8
+- stream.map vs forEach
+- Дополнительно
+ - [Сергей Куксенко — Stream API, часть 1](https://www.youtube.com/watch?v=O8oN4KSZEXE)
+ - [Сергей Куксенко — Stream API, часть 2](https://www.youtube.com/watch?v=i0Jr2l3jrDA)
+
+#### Optional 2 (+5 бонусов)
+```
+Сделать реализацию со сложностью O(N):
+- циклом за 1 проход по List. Обратите внимание на п.13 замечаний
+- через Stream API за 1 проход по исходному списку Stream meals
+ - возможно дополнительные проходы по частям списка
+ - нельзя использовать внешние коллекции, не являющиеся частью коллектора или результатами работы stream
+```
+#### Замечания по использованию Stream API:
+- Когда встречаешь что-то непривычное, приходится перестраивать мозги. Например, переход с процедурного на ООП программирование дается непросто. Те, кто не знает шаблонов (и не хотят учить) также их встречают плохо. Хорошая новость в том, что если это принять и начать использовать, то начинаешь получать от этого удовольствие. И тут главное не впасть в другую крайность:
+ - [Используйте Stream API проще (или не используйте вообще)](https://habrahabr.ru/post/337350/)
+- Если вас беспокоить производительность стримов, обязательно прочитайте про оптимизацию
+ - ["Что? Где? Когда?"](http://optimization.guide/intro.html)
+ - [Перформанс: что в имени тебе моём?](https://habrahabr.ru/company/jugru/blog/338732/)
+ - [Performance это праздник](https://habrahabr.ru/post/326242/)
+
+При использовании Stream API производительность улучшиться только на больших задачах, где возможно распараллеливание.
+Еще - просто так запустить и померять скорость JVM нельзя (как минимум дать прогреться и запустить очень большое число раз). Лучше использовать какие-нибудь бенчмарки, например [JMH](http://tutorials.jenkov.com/java-performance/jmh.html), который мы юзаем на другом проекте (Mastejava).
+
+##  Замечания к HW0
+- 1: Код проекта менять можно! Одна из распространенных ошибок как в тестовых заданиях на собеседовании, так и при работе на проекте, что ничего нельзя менять. Конечно при правках в рабочем проекте обязательно нужно проконсультироваться/проревьюироваться у авторов кода (находится по истории VCS)
+- 2: Наследовать `UserMealWithExceed` от `UserMeal` я не буду, т.к. это разные сущности: Transfer Object и Entity. Мы будет их проходить на 2м уроке.
+- 3: Правильная реализация должна быть простой и красивой, можно сделать 2-мя способами: через стримы и через циклы. Сложность должна быть O(N), т.е. без вложенных стримов и циклов.
+- 4: При реализации через циклы посмотрите в `Map` на методы `getOrDefault` или `merge`
+- 5: **При реализации через `Stream` заменяйте `forEach` оператором `stream.map(..)`**
+- 6: Объявляйте переменные непосредственно перед использованием (если возможно - сразу с инициализацией). При объявлении коллекций используйте тип переменной - интерфейс (Map, List, ..)
+- 7: Если IDEA предлагает оптимизацию (желтым подчеркивает), например заменить лямбду на метод-референс, соглашайтесь (Alt+Enter)
+- 8: Пользуйтесь форматированием кода в IDEA: `Alt+Ctrl+L`
+- 9: Перед check-in проверяйте чендж-лист (курсор на файл и Ctrl+D): не оставляйте в коде ничего лишнего (закомментированный код, TODO и пр.). Если файл не меняется (например только пробелы или переводы строк), не надо его чекинить, делайте ему `revert` (Git -> Revert / `Ctrl+Alt+Z`).
+- 10: `System.out.println` нельзя делать нигде, кроме как в `main`. Позже введем логирование.
+- 11: Результаты, возвращаемые `UserMealsUtil.getFilteredWithExceeded` мы будем использовать [в нашем приложении](http://topjava.herokuapp.com/) для фильтрации по времени и отображения еды правильным цветом.
+- 12: Обращайте внимание на комментарии к вашим коммитам в git. Они должны быть короткие и информативные (лучше на english)
+- 13: Не полагайтесь в решении на то, что список будет подаваться отсортированным. Такого условия нет.
+-----
+
+### Полезные ресурсы
+> ВНИМАНИЕ:
+> - ДЗ первого урока будет связано с созданием небольшого CRUD приложения (в памяти, без DB) на JSP и сервлетах. Введение будет, но предварительное знакомство не помешает.
+> - основы JavaSсript необходимы для понимания проекта, начиная с 8-го занятия!
+
+Все остальное - опционально.
+
+#### HTML, JavaScript, CSS
+- [Basic HTML and HTML5](https://learn.freecodecamp.org/responsive-web-design/basic-html-and-html5/say-hello-to-html-elements/)
+- [Справочник по WEB](https://developer.mozilla.org/ru/)
+- [Видео по WEB технологиям](https://www.youtube.com/user/WebMagistersRu/playlists)
+- [Изучение JavaScript в одном видео уроке за час](https://www.youtube.com/watch?v=QBWWplFkdzw)
+- HTML, CSS, JAVASCRIPT, SQL, JQUERY, BOOTSTRAP
+- Введение в программирование на JavaScript
+- Стандарты кодирования для HTML, CSS и JavaScript’a
+- Основы работы с HTML/CSS/JavaScript
+- JavaScript - Основы
+- Основы JavaScript
+- Bootstrap 3 - Основы
+- jQuery для начинающих
+
+#### Java (базовые вещи)
+- Интуит. Программирование на Java
+- 1й урок MasterJava: Многопоточность
+- Основы Java garbage collection
+- Размер Java объектов
+- Введение в Java Reflection API
+- Структуры данных в картинках
+- Обзор java.util.concurrent.*
+- Синхронизация потоков
+- String literal pool
+- Маленькие хитрости Java
+- A Guide to Java 8
+
+### Туториалы, разное
+[Что нужно знать о бэкенде новичку в веб-разработке](https://tproger.ru/translations/backend-web-development)
+[Туториалы: Spring Framework, Hibernate, Java Core, JDBC](http://proselyte.net/tutorials/)
+
+#### Сервлеты
+- Как создать Servlet? Полное руководство.
+
+#### JDBC, SQL
+- Основы SQL на примере задачи
+- Уроки по JDBC
+- Learn SQL
+- Интуит. Основы SQL
+- Try SQL
+- Курс "Введение в базы данных"
+
+#### Разное
+- Вопросы по собеседованию, ресурсы для подготовки
+- Эффективная работа с кодом в IntelliJ IDEA
+- Quizful- тесты онлайн
+- Введение в Linux
+
+#### Книги
+- Джошуа Блох: Java. Эффективное программирование. Второе издание
+- Гамма, Хелм, Джонсон: Приемы объектно-ориентированного проектирования. Паттерны проектирования
+- Редмонд Э.: Семь баз данных за семь недель. Введение в современные базы данных и идеологию NoSQL
+- Brian Goetz: Java Concurrency in Practice
+- G.L. McDowell: Cracking the Coding Interview
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
new file mode 100644
index 000000000000..c38eddb1fda2
--- /dev/null
+++ b/ReleaseNotes.md
@@ -0,0 +1,168 @@
+# TopJava Release Notes
+### Topjava 14
+- [Миграция на JUnit 5](https://www.youtube.com/watch?v=YmLzT-j1hU4)
+- Для измерения времени в тестах использую [Spring StopWatch](https://www.logicbig.com/how-to/code-snippets/jcode-spring-framework-stopwatch.html)
+- `SimpleJdbcInsert` и `NamedParameterJdbcTemplate` конструируются (и берут настройки) из `jdbcTemplate`
+- `AuthorizedUser` зарефакторился в `SecurityUtil`
+- В javascript [заменил `var` на `let/const`](https://learn.javascript.ru/let-const). [Поддержка 95% браузеров](https://caniuse.com/#feat=const)
+- Подправил UI фильтрации и заголовка страниц, добавилась кнопка `Cancel` в профиль
+- Починил [баг в FireFox](https://bugzilla.mozilla.org/show_bug.cgi?id=884693): пустой ответ по ajax
+- Сделал вход в приложение при нажании кнопок `Зайти как ...`
+- Добавил регистрацию пользователя по REST
+- Преименовал js файлы согласно [javascript filename naming convention](https://stackoverflow.com/questions/7273316/what-is-the-javascript-filename-naming-convention)
+- Сделал проверку startTime/endTime на фильтре времени (после обновления datetimepicker до 2.5.20)
+
+### Topjava 13
+- [Миграция на Botstrap 4](https://getbootstrap.com/docs/4.1/migration/)
+- Добавил [Responsive behaviors](https://getbootstrap.com/docs/4.1/components/navbar/#responsive-behaviors) - при уменшении ширины экрана навигация сворачивается в кнопку
+- Для отображения цвета еды и выключенного юзера использую [data-* атрибуты](https://developer.mozilla.org/ru/docs/Web/Guide/HTML/Using_data_attributes)
+- В `inputField.tag` передаю как параметр код для локализации label, а в `i18n.jsp` передаю как параметр `page`. См. [JSP include action with parameter example](https://beginnersbook.com/2013/12/jsp-include-with-parameter-example)
+
+### Topjava 12
+- обновил версии: Spring 5.x, Spring Data 2.x, Ehcache 3.x, datatables, datetimepicker
+- добавил видео решений HW0 с одним проходом
+- поправил видео [Обзор Spring Framework. Spring Context](https://drive.google.com/file/d/1fBSLGEbc7YXBbmr_EwEHltCWNW_pUmIH). Дописал про Constructor injection.
+- заменил видео про тетсирование сервисов. Вместо самодельных матчеров стали использовать [AssertJ](http://joel-costigliola.github.io/assertj/index.html). Видео [Тестирование UserService через AssertJ](https://drive.google.com/open?id=1SPMkWMYPvpk9i0TA7ioa-9Sn1EGBtClD), время 1:53
+- сделал [видео с jQuery конвертерами и дефолтными группами валидации при сохранении в базу](https://drive.google.com/open?id=1tOMOdmaP5OQ7iynwC77bdXSs-13Ommax)
+- сделал [видео с новым `DelegatingPasswordEncoder` и Json READ/WRITE access](https://drive.google.com/file/d/1XZXvOThinzPw4EhigAUdo8-MWT_g8wOt/view?usp=sharing)
+- убрал `AccessType.PROPERTY` для `AbstractBaseEntity.id` (см. [fixed HHH-3718](https://hibernate.atlassian.net/browse/HHH-3718))
+- удалил `PasswordUtil`, возвращаю статус `NO_CONTENT` для REST delete, убрал группы валидации в `UserTo`
+- заменил в jQuery [success на done](https://stackoverflow.com/a/22213543/548473)
+- вместо `lang.jsp` сделал общий `bodyHeader.jsp`
+
+### Topjava 11
+- добавил
+ - доп. решение HW1 через одним return и O(N)
+ - раскрасил лог ([Logback layouts coloring](https://logback.qos.ch/manual/layouts.html#coloring))
+- рефакторинг
+ - починил коммит формы по cancel (`history.back()`) в FireFox
+ - заменил неработающий DependencyCi на [VersionEye](https://www.versioneye.com/) c проверкой зависимостей на uptodate
+ - починил `CrudUserRepository.getWithMeals()` через `@EntityGraph`. С неколькими ролями (у админа) еда дублируется
+ - починил тесты контроллеров с профилем JDBC (`JpaUtil` отсутствует в контексте JDBC)
+ - переименовал `meal.jsp/user.jsp` в `mealForm.jsp/userForm.jsp`
+ - в `InMemoryMealRepositoryImpl.save()` сделал update атомарным
+ - переименовал методы сервисов `save` в `create`
+ - переименовал и cделал классы `BaseEntity` и `NamedEntity` абстрактными
+ - обновил Noty и API с ним до 3.1.0. Добавил glyphicon в сообщения Noty
+ - заменил `MATCHER_WITH_EXCEED` на валидацию через [JSONassert](https://github.com/skyscreamer/JSONassert).
+ - поменял Deprecated валидаторы `org.hibernate.validator.constraints` на `javax.validation.constraints`
+ - убрал пароль из результатов REST через [@JsonProperty READ_ONLY / WRITE_ONLY](https://stackoverflow.com/questions/12505141/only-using-jsonignore-during-serialization-but-not-deserialization/12505165#12505165). Тесты на REST пришлось починить добавлением добавлением в JSON пароля как дополнительного параметра (`JsonUtil.writeWithExtraProps`)
+ - **убрал JSON View и сделал преобразование времени на UI с помощью [jQuery converters](http://api.jquery.com/jQuery.ajax/#using-converters)**
+ - **поменял [группу валидации по умолчанию при сохранении через JPA](https://stackoverflow.com/questions/16930623/16930663#16930663).** Теперь
+ все валидаторы в модели работаю по умолчанию (`groups` не требуется).
+ - Добавил в `ErrorInfo` тип ошибки `ErrorType` + i18n.
+
+- правки
+ - переименовал `ModelMatcher` в `BeanMatcher` и починил: можно сравнивать только упорядоченные коллекции (List)
+ - поменял зависимость `org.hibernate:hibernate-validator` на `org.hibernate.validator:hibernate-validator` (warning при сборке)
+
+### Topjava 10
+- добавил
+ - доступ к AuthorizedUser через [`@AuthenticationPrincipal`](http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-authentication-principal) и [authentication Tag](http://docs.spring.io/spring-security/site/docs/current/reference/html/taglibs.html#the-authentication-tag)
+ - [Обработку 404 NotFound](https://stackoverflow.com/questions/18322279/spring-mvc-spring-security-and-error-handling)
+ - локализацию ошибок валидации
+ - проверки json в тестах через [JSONassert](https://github.com/skyscreamer/JSONassert) и [через jsonPath](https://www.petrikainulainen.net/programming/spring-framework/integration-testing-of-spring-mvc-applications-write-clean-assertions-with-jsonpath/)
+ - [логирование от Postgres Driver](http://stackoverflow.com/a/43242620/548473)
+ - в `.travis.yml` [сборку только ветки master](https://docs.travis-ci.com/user/customizing-the-build#Building-Specific-Branches)
+ - [защиту от кэширование ajax запросов в IE](https://stackoverflow.com/a/4303862/548473)
+ - обработку запрета модификации системный юзеров через универсальный `ApplicationException`
+ - рефакторинг
+ - сделал `@EntityGraph` через `attributePaths`
+ - реализаовал обработку дублирования `user.email` и `meal.dateTime` через [Controller Based Exception Handling](https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc#controller-based-exception-handling)
+ - поменял отключение транзакционности в тестах через `@Transactional(propagation = Propagation.NEVER)`
+ - сделал выбор в сервлете через switch
+ - [все логгирование сделал через {} форматирование](http://stackoverflow.com/questions/10555409/logger-slf4j-advantages-of-formatting-with-instead-of-string-concatenation) и поправил его в контроллерах (поле проверки id)
+ - [перешел на конструктор DI](http://stackoverflow.com/questions/39890849/what-exactly-is-field-injection-and-how-to-avoid-it)
+ - в `ModelMatcher` переименовал `Comparator` -> `Equality`
+ - [заинлайнил все лямбды](http://stackoverflow.com/questions/19718353/is-repeatedly-instantiating-an-anonymous-class-wasteful) (компараторы, ModelMatcher.equality)
+ - поменялась реализация `JdbcUserRepositoryImpl.getAll()`
+ - на UI кнопки в таблице заменились на линки, поправил сообщения локализации
+ - [сделал кастомизацию JSON (@JsonView) и валидацию (groups)](https://drive.google.com/file/d/0B9Ye2auQ_NsFRTFsTjVHR2dXczA) для данных еды, отдаваемых на UI
+ - в `JdbcUserRepositoryImpl` поменял `MapSqlParameterSource` на `BeanPropertySqlParameterSource`
+- удалил
+ - зависимость `javax.transaction.jta` (уже не нужна)
+ - `${spring.version}` в `pom.xml` зависимостях (уже есть в `spring-framework-bom`)
+ - distinct из запроса Hibernate на пользователей с ролями. [Оптимизация запроса distinct: 15.16.2](https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#hql-distinct)
+ - лишние `
` тэги (`shadow` и `view-box`)
+
+### Topjava 9
+- добавил
+ - выбор профиля базы через `ActiveProfilesResolver`/`AllActiveProfileResolver` на основе драйвера базы в classpath
+ - видео Cascade. Auto generate DDL.
+ - проверку на правильность id в Ajax/Rest контроллерах (treat IDs in REST body)
+ - тесты на валидацию входных значений контроллеров и зависимость на имплементацию
+ - Bootstrap Glyphicons
+- рефакторинг
+ - переименовал `TimeUtil` в `DateTimeUtil`
+ - переименовал `ExceptionUtil` в `ValidationUtil`
+ - заменил валидацию `@NotEmpty` на `@NotBlank`
+ - заменил `CascadeType.REMOVE` на `@OnDelete`
+ - изменил `JdbcUserRepositoryImpl.getAll()`
+ - обновил jQuery до 3.x, исключил из зависимостей webjars ненужные jQuery
+ - cделал загрузку скриптов асинхронной
+ - фильтр еды сделал в [Bootstrap Panels](http://getbootstrap.com/components/#panels)
+ - вместо `Persistable` ввел интерфейс `HasId` и наследую от него как Entity, так и TO
+ - сделал универсальную обработку исключений дублирования email и dateTime
+
+### Topjava 8
+- добавил:
+ - [защиту от XSS (Cross-Site Scripting)](http://stackoverflow.com/a/40644276/548473)
+ - интеграцию с Dependency Ci и Travis Ci
+ - локализацию календаря
+ - сводку по результатам тестов
+ - примеры запросов curl в `config/curl.md`
+ - DataTables/Bootstrap 3 integration
+ - тесты на профиль деплоя Heroku (общее количество JUnit тестов стало 102)
+- удалил зависимость `jul-to-slf4j`
+- рефакторинг
+ - переименовал все классы `UserMeal**` в `Meal**`, JSP
+ - переименовал `LoggedUser` в `AuthorizedUser`
+ - починил работа с PK Hibernate в случае ленивой загрузки (баг HHH-3718)
+ - поменял в `BaseEntity` `equals/hashCode/implements Persistable`
+ - в `InMemoryMealRepositoryImpl` выделил метод `getAllStream`
+ - перенес проверки пердусловий `Assert` из `InMemory` репозиториев в сервисы
+ - переименовал классы _Proxy*_ на более адекватные _Crud*_
+ - поменял реализацию `JpaMealRepositoryImpl.get`, добавил в JPA модель `@BatchSize`
+ - вместо `@RequestMapping` ввел Spring 4.3 аннотации `@Get/Post/...Mapping`
+ - поменял авторизацию в тестах не-REST контроллеров
+ - перенес вызовы `UserUtil.prepareToSave` из `AbstractUserController` в `UserServiceImpl`
+ - зарефакторил обработку ошибок (`ExceptionInfoHandler`)
+
+### Topjava 7
+- добавил:
+ - [JPA 2.1 EntityGraph](https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs002.htm)
+ - [Jackson @JsonView](https://habrahabr.ru/post/307392/)
+ - валидацию объектов REST
+ - [i18n в JavaScript](http://stackoverflow.com/a/6242840/548473)
+ - проверку предусловий и видео Методы улучшения качества кода
+ - интеграцию с проверкой кода в Codacy
+ - [сравнение вермени исполнения запросов при различных meals индексах](https://drive.google.com/open?id=0B9Ye2auQ_NsFX3RLcnJCWmQ2Y0U)
+- tomcat7-maven-plugin плагин перключили на Tomcat 8 (cargo-maven2-plugin)
+- рефакторинг
+ - обработка ошибок сделал с array
+ - матчеров тестирования (сделал автоматические обертки и сравнение на основе передаваемого компаратора)
+ - вынес форматирование даты в `functions.tld`
+
+### Topjava 3-6
+- добавил
+ - [выпускной проект](https://drive.google.com/open?id=0B9Ye2auQ_NsFcG83dEVDVTVMamc)
+ - в таблицу meals составной индекс
+ - константы `Profiles.ACTIVE_DB`, `Profiles.DB_IMPLEMENTATION`
+ - проверки и тесты на `NotFound` для `UserMealService.getWithUser` и `UserService.getWithMeals`
+ - в MockMvc фильтр CharacterEncodingFilter
+ - защиту от межсайтовой подделки запроса, видео Межсайтовая подделка запроса (CSRF)
+ - ограничение на диапазон дат для фильтра еды
+- рефакторинг
+ - UserMealsUtil, ProfileRestController, компараторов в репозитоии
+ - `LoggedUser` отнаследовал от `org.springframework.security.core.userdetails.User`
+ - переименовал `DbTest` в `AbstractServiceTest` и перенес сюда `@ActiveProfiles`
+ - сделал выполнение скриптов в тестах через аннотацию `@Sql`
+ - вместо использования id и селектора сделал обработчик `onclick`
+ - изменил формат ввода даты в форме без 'T'
+- убрал
+ - `LoggerWrapper`
+ - Dandelion обертку к datatables
+- обновил
+ - Hibernate до 5.x и Hibernate Validator, добавились новые зависимости и `jackson-datatype-hibernate5`
+ - datatables API (1.10)
+ - Postgres драйвер. Новый драйвер поддерживает Java 8 Time API, разделил реализацию JdbcMealRepositoryImpl на Java8 (Postgresql) и Timestamp (HSQL)
diff --git a/cv.md b/cv.md
new file mode 100644
index 000000000000..e78b7faedc2b
--- /dev/null
+++ b/cv.md
@@ -0,0 +1,107 @@
+## Составление резюме, подготовка к интервью, поиск работы
+
+
+
+- Научиться программировать сложнее, чем кажется
+- [Собеседование. Разработка ПО. Вопросы.](https://drive.google.com/open?id=0B9Ye2auQ_NsFQVc2WUdCR0xvLWM)
+- [Набор ссылок для тренировки и прохождения интервью](https://github.com/andreis/interview)
+
+### Составление резюме:
+- [VisualCV: create resume in minutes](https://www.visualcv.com/)
+- Выбрать шаблон для резюме
+- [GitHub Pages](https://pages.github.com/), Resume template
+- Как продать свое резюме в 2 раза дороже
+- Как правильно составить резюме
+- Резюме программистов. Часть 1 (плохие)
+- Резюме программистов. Часть 2 (хорошие)
+- Как составить резюме на английском
+- ОФОРМЛЕНИЕ IT-РЕЗЮМЕ для USA
+
+### Наши истории (делимся опытом и успехом)
+
+### Тесты/задачи онлайн:
+- [Java Programming Test](https://tests4geeks.com/java)
+- game: test Java skills
+- Codility lesson tests
+- Quizful- тесты онлайн
+- LeetCode Online Judge
+- Sphere online judge
+- Codility programmers lessons
+- Hackerrank practice coding
+
+## [Тестовое собеседование, самые спрашиваемые темы](http://javaops.ru/interview/test.html)
+
+### Интервью:
+- Михаил Портнов. Собеседование на работу: как продать себя грамотно
+- Михаил Портнов. Какие вопросы мы задаем на собеседовании?
+- Михаил Портнов. Собеседование на работу: жизненный путь
+- Канал: Резюме, поиск работы, интервью
+- Яков Файн: Как стать профессиональным Java разработчиком
+- Ответы на вопросы на собеседовании Junior Java Developer
+- Список вопросов с ответами для собеседования по Java
+- Сборка по вопросам на интервью
+- Сборка вопросов-ответов от JavaStudy
+- [Вопросы по классам коллекциям от JavaRush-1](http://info.javarush.ru/translation/2013/10/08/Часто-задаваемые-на-собеседованиях-вопросы-по-классам-коллекциям-в-Java-Часть-1-.html)
+- [Вопросы по классам коллекциям от JavaRush-2](http://info.javarush.ru/translation/2013/10/08/Часто-задаваемые-на-собеседованиях-вопросы-по-классам-коллекциям-в-Java-Часть-2-.html)
+- Тест на знание SQL
+- Вопросы на собеседовании Java Junior Developer
+- Java вопросы с собеседований на Android
+- Сборка вопросов от JavaRush
+> про clone и finalize объязательно прочтите Джошуа Блох: Java. Эффективное программирование (второе издание)
+
+- Cracking the Coding Interview
+> Особенно обратите внимание на раздел: Часть VIII. Вопросы собеседования
+
+
+### От себя:
+- email, skype - очень желательно, чтобы по ним вы были узнаваемы. Заведите рабочие, если не так.
+- написать ВЕСЬ IT опыт (исключая опыт пользователя: Windows, MS Word, Photophop, Yandex disk, Google docs, ..): технологии, какие задачи решали (конкретные), какие инструменты использовали, VCS, DB, инструменты сборки, ... включая опыт в ВУЗе.
+- на English иметь желательно. Если вакансия опублинована на Englsih - шлите на нем. Часто могут на нем попросить, если работодатель иностранный.
+- удобно иметь резюме где то в инете (hh, linkedin, google doc, чтобы им было удобно делиться).
+
+### Позиционирование проекта Topjava:
+- Обязательно убери из резюме **любое упоминание Junior**. Количество обращений возрастет на порядок. Ссылку на стажировку можно поставить: http://javaops.ru/view/topjava (в linkedin: https://www.linkedin.com/company/java-online-projects).
+- После завершения проекта ты освошь все заявленные в нем технологии - вставь их в квалификацию (включая java 8 Stream and Time API).
+- В разделе опыт работы (если нет коммерческого опыта) вставь:
+
+ Участие в разработке Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей
+ на стеке Maven/ Spring MVC/ Security/ REST(Jackson)/ Java 8 Stream API:
+ - реализация сохранения в базы Postgres и HSQLDB на основе Spring JBDC, JPA(Hibernate) и Spring-Data-JPA
+ - реализация и тестирование REST и AJAX контроллеров
+ - реализация клиента на Bootstrap (css/js), datatables, jQuery + plugins.
+ - собственная доработка проекта
+
+- Делай упор не на обучение, а на **участие в проекте**. Выполнение домашних заданий это полноценное участие с написанием функционала по всем пройденным технологиям. На собеседовании смотрят не на то, что ты заканчивал, а на опыт и знания.
+
+### В процессе обучения
+- Если рассмотриваешь предложения по работе, подними в своем профиле этот флаг и обязательно заполни ссылку на резюме. Обновления нашей базы выпускников смотрят уже более 125 партнеров по трудоустройству (компании и индивидуальные рекрутеры). Проверь содержание "Информация для HR": по нему принимают решение, открывать резюме или нет.
+
+- Вступайте в нашу группу участников Slack: каналы помощи с Java, отзывы о работодателях, обсуждение тестовых заданий, вакансии, цены на рынке труда, IT события, интересные видео и многое другое.
+
+- Подпишитесь на рассылку вакансий под себя
+
+### После прохождения испытательного срока жду твою [историю успеха](http://javaops.ru/view/story)
+
+### Основные сайты поиска работы:
+- Яндекс агрегатор
+- HH
+- LinkedIn
+- djinni.co (более актуально для Украины)
+
+## Как выжить на испытательном сроке
+- Учись грамотно формулировать проблему. Проблема "у меня не работает" может иметь тысячи причин. В
+ процессе формулирования очень часто приходит ее решение.
+- Учись инвестигировать проблему. Внимательное чтение логов и умение дебажить - основные навыки
+ разработчика. В логах надо читать верх самого нижнего эксепшена - там причина всей портянки.
+- Грамотно уделяй время каждой проблеме. Две крайности - сразу бросаться за помощью и
+ бится нам ней часами.
+ Пробуй решить ее сам и в зависимости от проблемы выделяй на это разумное время.
+- Если тебе что-то объясняют по проекту - обязательно записывай.
+- Когда получаешь задачу - уточни все очень подробно.
+- Получай в процессе решения обратную связь - в том ли направлении ты идешь.
+- Не игнорируй совместные ланчи (курилки)
+- Готовься к стендапам/летучкам. Задавай на них вменяемые вопросы. Выказывай заинтересованность
+- Выдели самое главное путем опроса босса и важных коллег. Не распыляйся на мелочи.
+- [**Советы новичкам**](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..d2448ca99e57
--- /dev/null
+++ b/description.md
@@ -0,0 +1,77 @@
+#### Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей с использованием наиболее популярных инструментов и технологий Java: Maven, Spring MVC, Security, JPA(Hibernate), REST(Jackson), Bootstrap (css,js), datatables, jQuery + plugins, Java 8 Stream and Time API и сохранением в базах данных Postgresql и HSQLDB.
+
+- Основное внимание будет уделяться способам решения многочисленных проблем разработки в Spring/JPA, а также структурному (красивому и надежному) java кодированию и архитектуре приложения.
+- Каждая итерация проекта закрепляется домашним заданием по реализации схожей функциональности. Следующее занятие начинается с разбора домашних заданий.
+- Большое внимание уделяется тестированию кода: в проекте более 100 JUnit тестов.
+- Несмотря на относительно небольшой размер, приложение разрабатывается с нуля как большой проект (например мы используем кэш 2-го уровня Hibernate, настраиваем Jackson для работы с ленивой загрузкой
+Hibernate, делаем конверторы для типов LocalDateTime (Java 8 time API).
+ Разбираются архитектурные паттерны: слои приложения и как правильно разбивать логику по слоям, когда нужно применять Data Transfer Object.
+ Т.е на выходе получается не учебный проект, а хорошо маштабируемый шаблон для большого проекта на всех пройденных технологиях.
+- Большое внимание уделяется деталям: популяция базы, использование транзакционности, тесты сервисов и REST
+ контроллеров, насторойка EntityManagerFactory,
+ выбор реализации пула коннектов. Особое внимание уделяется работе с базой: через Spring JDBC, Spring ORM и
+ Spring Data Jpa.
+- Используются самые востребованные на сегодняшний момент фреймворки: Maven, Spring Security 4
+ вместе с Spring Security Test, наиболее удобный для работы с базой проект Spring Data Jpa, библиотека логирования logback, реализующая SLF4J, повсеместно используемый Bootstrap и jQuery.
+
+#### Демо разрабатываемого приложения
+
+## План проекта (ссылки на некоторые темы открыты для просмотра)
+### Архитектура проекта. Персистентность.
+- Системы управления версиями
+- Java 8: Lambda, Stream API
+- Обзор используемых в проекте технологий и инструментов.
+- Инструмент сборки Maven.
+- WAR. Веб-контейнер Tomcat. Сервлеты.
+- Логирование.
+- Обзор стандартных библиотек. Apache Commons, Guava
+- Слои приложения. Создание каркаса приложения.
+- Обзор Spring Framework. Spring Context.
+- Тестирование через JUnit.
+- Spring Test
+- Базы данных. PostgreSQL. Обзор NoSQL и Java persistence solution без ORM.
+- Настройка Database в IDEA.
+- Скрипты инициализации базы. Spring Jdbc Template.
+- Spring: инициализация и популирование DB
+- ORM. Hibernate. JPA.
+- [Тестирование JPA сервиса через AssertJ](https://www.youtube.com/watch?v=BlyaXT6tOaw)
+- Поддержка HSQLDB
+- Транзакции
+- Профили Maven и Spring
+- Пул коннектов
+- Spring Data JPA
+- Кэш Hibernate
+
+### Разработка WEB
+- Spring кэш
+- Spring Web
+- JSP, JSTL, i18n
+- Tomcat maven plugin. JNDI
+- Spring Web MVC
+- Spring Internationalization
+- Тестирование Spring MVC
+- REST контроллеры
+- Тестирование REST контроллеров. Jackson.
+- jackson-datatype-hibernate. Тестирование через матчеры.
+- Тестирование через SoapUi. UTF-8
+- WebJars.
+- Bootstrap. jQuery datatables.
+- AJAX. jQuery. Notifications.
+- Spring Security
+- Spring Binding/Validation
+- Работа с datatables через Ajax.
+- Spring Security Test
+- [Кастомизация JSON (@JsonView) и валидации (groups)](https://drive.google.com/open?id=0B9Ye2auQ_NsFRTFsTjVHR2dXczA)
+- Encoding password
+- CSRF (добавление в проект защиты от межсайтовой подделки запроса)
+- form-login. Spring Security Taglib
+- Handler interceptor
+- Spring Exception Handling
+- Смена локали
+- Фильтрация JSON через @JsonView
+- Защита от XSS (Cross Site Scripting)
+- Деплой в Heroku
+- Локализация datatables, ошибок валидации
+- Обработка ошибок 404 (NotFound)
+- Доступ к AuthorizedUser
+- Собеседование. Разработка ПО
diff --git a/graduation.md b/graduation.md
new file mode 100644
index 000000000000..1a38bb78e347
--- /dev/null
+++ b/graduation.md
@@ -0,0 +1,67 @@
+## Выпускной проект
+Design and implement a REST API using Hibernate/Spring/SpringMVC (or Spring-Boot) **without frontend**.
+
+The task is:
+
+Build a voting system for deciding where to have lunch.
+
+ * 2 types of users: admin and regular users
+ * Admin can input a restaurant and it's lunch menu of the day (2-5 items usually, just a dish name and price)
+ * Menu changes each day (admins do the updates)
+ * Users can vote on which restaurant they want to have lunch at
+ * Only one vote counted per user
+ * If user votes again the same day:
+ - If it is before 11:00 we asume that he changed his mind.
+ - If it is after 11:00 then it is too late, vote can't be changed
+
+Each restaurant provides new menu each day.
+
+As a result, provide a link to github repository. It should contain the code, README.md with API documentation and couple curl commands to test it.
+
+-----------------------------
+P.S.: Make sure everything works with latest version that is on github :)
+
+P.P.S.: Asume that your API will be used by a frontend developer to build frontend on top of that.
+
+-----------------------------
+###  Рекомендации
+
+- **Если ты оканчивал [стажировку Topjava](http://javaops.ru/reg/topjava/grd), cделай новый проект и добавляй туда из Topjava только то что нужно! Локализация, типы ошибок, BeanMatcher, Json View, излишние делегирования и наследования - не нужны!**
+- **API продумывай с точки зрения не программиста и объектов, а с точки зрения того, кто им будет пользоваться (frontend)**
+- **Сначала сделай основной сценарий по ТЗ. Все остальное (если очень хочется, 3 раза подумай) - потом.**
+
+*Представьте себе, что ПМ (лид, архитектор) дал вам ТЗ и некоторое время недоступен. У вас конечно есть много мыслей, для чего нужно приложение, как исправить ТЗ, дополнить его и сделать правильно. НО НЕ НАДО ИХ РЕАЛИЗОВЫВАТЬ В КОДЕ. Нужно сделать все максимально просто, удобно для доработок и для использования со стороны клиента (если конечно в ТЗ нет оговорок). Все свои вопросы и предложения и хотелки оформляйете отдельно (в `read.me` например). Если делаете что-то сложнее простейшего случая (например справочник еды)- объязательно напишите в read.me. Как и выбор стратегии кэширования.*
+
+> Совершенство достигнуто не тогда, когда нечего добавить, а тогда, когда нечего отнять
+
+_Антуан де Сент-Экзюпери_
+
+- 1: **читаем ТЗ ОЧЕНЬ внимательно, НЕ надо ничего своего туда домысливать и творчески изменять**
+- 2: **тщательно считайте количество обращений в базу на каждый запрос. Особенно при запросах от юзеров, которых очень много! Также на сложность запросов от них, чтобы не положить базу**
+- 3: **тщательно считайте количество запросов в вашем API для отображения нужной информации**
+- 4: **учитывайте, что пользователей может быть ооочень много, а админов- мало**
+- 5: в проекте (и тестовом задании на работу) в отличие от нашего учебного topjava оставляйте только необходимый для работы приложения код, ничего лишнего:
+ - 5.1 НЕ надо делать разные профили базы и работы с ней.
+ - 5.2 НЕ надо делать абстрактных контроллеров на всякий случай.
+ - 5.3 НЕ надо делать **классов репозиториев и сервисов**, если там нет ничего, кроме делегирования.
+ - 5.4 Из потребностей приложения (которую надо самим придумать) реализовывать только очевидные сценарии. Те.- НИЧЕГО ЛИШНЕГО.
+- 6: базу лучше взять без установки (H2 или HSQLDB)
+- 7: по возможности сделать JUnit тесты
+- 8: уделяйте внимание обработке ошибок
+- 9: далаем REST API в соответствии с концепцией REST, **с учетом иерархии принадлежности объектов**
+ - [15 тривиальных фактов о правильной работе с протоколом HTTP](https://habrahabr.ru/company/yandex/blog/265569/)
+ - 10 Best Practices for Better RESTful API
+- 10: не смешивайте TO и Entity вместе. Лучше всего, если они будут независимыми друг от друга.
+- 11: если приложению в объекте требуется только его id, используйте reference (как мы при сохранении еды вставляем туда юзера)
+- 12: [Use for money in java app](http://stackoverflow.com/a/43051227/548473)
+- 13: **Историю еды и голосований лучше сделать. Нужно различать базовые вещи, которые закладываются в архитектуру приложения и неочевидные доработки к ТЗ, которых лучше не делать.**
+- 14: Еще раз про [hashCode/equals в Entity](https://stackoverflow.com/questions/5031614/the-jpa-hashcode-equals-dilemma): не делайте сравнение по всем полям!
+- 15: Название пакетов, имен классов для `model/to/web` достаточно стандартные (например `model/domain`). НЕ надо придумывать своих собственных правил.
+- 16: *Предпочтительно использовать DATA-JPA* (можно без лишней делегации, напрямую из сервиса/контроллера дергать Repository). В случае JPA позаботьтесь о своем собственном generic DAO.
+- 17: В DATA-JPA 2.x используются `Optional`. Попробуйте работать с ними, это безопасный способ работать с null значениями.
+- 18: На topjava мы смотрели разные варианты использования, тут делаем максимально просто. С TO многие вещи упрощаются.
+- 19: Проверьте, не торчат ли из кода учебные уши topjava, типа `ProfileRestController.testUTF()`, `AbstractServiceTest.printResult()` или закомментированные `JdbcTemplate`. Назначение этого проекта совсем другое.
+- 20: ORM работает с объектами. [В простейших случаях fk_id как поля допустимы](https://stackoverflow.com/questions/6311776/hibernate-foreign-keys-instead-of-entities), но при этом JPA их уже никак не обрабатывает и не используйте их вместе с объектами. Ссылка на stackoverwrflow в коде обязательна!
+- 21: Проверьте, станет ли код проще с `@AuthenticationPrincipal` (урок 11, Доступ к AuthorizedUser). С ним можно убрать из `AuthorizedUser` все статические методы.
+- 22: Не размещайте логику приложения и преобразования в TO в слое доступа к DB
+- 23: Если используете кэширование, тщательно продумайте, что надо кэшировать (самые частые запросы), а что нет (большие или редкозапрашиваемые данные)!
diff --git a/pom.xml b/pom.xml
index c8a1c78f3b29..d3da38319f77 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
ru.javawebinartopjava
- jar
+ war1.0-SNAPSHOT
@@ -15,25 +15,149 @@
1.8UTF-8UTF-8
+
+ 5.1.0.RELEASE
+
+
+ 1.2.3
+ 1.7.25
+
+
+ 42.2.5
+
+ 4.12
+
+
+ 5.3.7.Final
+ 6.0.13.Final
+ 3.0.1-b10topjava
- install
+ packageorg.apache.maven.pluginsmaven-compiler-plugin
- 3.1
+ 3.7.0${java.version}${java.version}
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.0
+
+ -Dfile.encoding=UTF-8
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+ compile
+
+
+
+ org.slf4j
+ jul-to-slf4j
+ ${slf4j.version}
+ runtime
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ runtime
+
+
+
+
+ org.springframework
+ spring-context
+ ${spring.version}
+
+
+ org.springframework
+ spring-orm
+ ${spring.version}
+
+
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+
+
+ org.hsqldb
+ hsqldb
+ 2.3.4
+
+
+
+
+
+ org.hibernate
+ hibernate-core
+ ${hibernate.version}
+
+
+ org.hibernate.validator
+ hibernate-validator
+ ${hibernate-validator.version}
+
+
+
+
+ org.glassfish
+ javax.el
+ ${javax-el.version}
+ provided
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 4.0.0
+ provided
+
+
+
+ javax.servlet
+ jstl
+ 1.2
+
+
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+ org.springframework
+ spring-test
+ ${spring.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.11.1
+ test
+
diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java
deleted file mode 100644
index b23a2f0961fc..000000000000
--- a/src/main/java/ru/javawebinar/topjava/Main.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package ru.javawebinar.topjava;
-
-/**
- * User: gkislin
- * Date: 05.08.2015
- *
- * @link http://caloriesmng.herokuapp.com/
- * @link https://github.com/JavaOPs/topjava
- */
-public class Main {
- public static void main(String[] args) {
- System.out.format("Hello Topjava Enterprise!");
- }
-}
diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java
new file mode 100644
index 000000000000..91fbfca426cc
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java
@@ -0,0 +1,57 @@
+package ru.javawebinar.topjava.model;
+
+import javax.persistence.*;
+
+@MappedSuperclass
+// http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access
+@Access(AccessType.FIELD)
+public abstract class AbstractBaseEntity {
+ public static final int START_SEQ = 100000;
+
+ @Id
+ @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ)
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq")
+ protected Integer id;
+
+ protected AbstractBaseEntity() {
+ }
+
+ 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);
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ AbstractBaseEntity that = (AbstractBaseEntity) o;
+ return id != null && id.equals(that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id == null ? 0 : id;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java
new file mode 100644
index 000000000000..e0b51ebfa8fe
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java
@@ -0,0 +1,37 @@
+package ru.javawebinar.topjava.model;
+
+import javax.persistence.Column;
+import javax.persistence.MappedSuperclass;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+
+@MappedSuperclass
+public abstract class AbstractNamedEntity extends AbstractBaseEntity {
+
+ @NotBlank
+ @Size(min = 2, max = 100)
+ @Column(name = "name", nullable = false)
+ protected String name;
+
+ protected AbstractNamedEntity() {
+ }
+
+ protected AbstractNamedEntity(Integer id, String name) {
+ super(id);
+ this.name = name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Entity %s (%s, '%s')", getClass().getName(), id, name);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java
new file mode 100644
index 000000000000..788ae8f6fe5b
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java
@@ -0,0 +1,82 @@
+package ru.javawebinar.topjava.model;
+
+import javax.persistence.FetchType;
+import javax.persistence.ManyToOne;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+public class Meal extends AbstractBaseEntity {
+ private LocalDateTime dateTime;
+
+ private String description;
+
+ private int calories;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private User user;
+
+ public Meal() {
+ }
+
+ public Meal(LocalDateTime dateTime, String description, int calories) {
+ this(null, dateTime, description, calories);
+ }
+
+ public Meal(Integer id, LocalDateTime dateTime, String description, int calories) {
+ super(id);
+ this.dateTime = dateTime;
+ this.description = description;
+ this.calories = calories;
+ }
+
+ public LocalDateTime getDateTime() {
+ return dateTime;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public LocalDate getDate() {
+ return dateTime.toLocalDate();
+ }
+
+ public LocalTime getTime() {
+ return dateTime.toLocalTime();
+ }
+
+ public void setDateTime(LocalDateTime dateTime) {
+ this.dateTime = dateTime;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public void setCalories(int calories) {
+ this.calories = calories;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ @Override
+ public String toString() {
+ return "Meal{" +
+ "id=" + id +
+ ", dateTime=" + dateTime +
+ ", description='" + description + '\'' +
+ ", calories=" + calories +
+ '}';
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java
new file mode 100644
index 000000000000..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..cc2f0a36fe95
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/model/User.java
@@ -0,0 +1,136 @@
+package ru.javawebinar.topjava.model;
+
+import org.hibernate.validator.constraints.Range;
+import org.springframework.util.CollectionUtils;
+
+import javax.persistence.*;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.*;
+
+import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY;
+
+@NamedQueries({
+ @NamedQuery(name = User.DELETE, query = "DELETE FROM User u WHERE u.id=:id"),
+ @NamedQuery(name = User.BY_EMAIL, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email=?1"),
+ @NamedQuery(name = User.ALL_SORTED, query = "SELECT u FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email"),
+})
+@Entity
+@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "users_unique_email_idx")})
+public class User extends AbstractNamedEntity {
+
+ public static final String DELETE = "User.delete";
+ public static final String BY_EMAIL = "User.getByEmail";
+ public static final String ALL_SORTED = "User.getAllSorted";
+
+ @Column(name = "email", nullable = false, unique = true)
+ @Email
+ @NotBlank
+ @Size(max = 100)
+ private String email;
+
+ @Column(name = "password", nullable = false)
+ @NotBlank
+ @Size(min = 5, max = 100)
+ private String password;
+
+ @Column(name = "enabled", nullable = false, columnDefinition = "bool default true")
+ private boolean enabled = true;
+
+ @Column(name = "registered", columnDefinition = "timestamp default now()")
+ @NotNull
+ private Date registered = new Date();
+
+ @Enumerated(EnumType.STRING)
+ @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
+ @Column(name = "role")
+ @ElementCollection(fetch = FetchType.EAGER)
+ private Set roles;
+
+ @Column(name = "calories_per_day", columnDefinition = "int default 2000")
+ @Range(min = 10, max = 10000)
+ private int caloriesPerDay = DEFAULT_CALORIES_PER_DAY;
+
+ public User() {
+ }
+
+ public User(User u) {
+ this(u.getId(), u.getName(), u.getEmail(), u.getPassword(), u.getCaloriesPerDay(), u.isEnabled(), u.getRegistered(), u.getRoles());
+ }
+
+ public User(Integer id, String name, String email, String password, Role role, Role... roles) {
+ this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), EnumSet.of(role, roles));
+ }
+
+ public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) {
+ super(id, name);
+ this.email = email;
+ this.password = password;
+ this.caloriesPerDay = caloriesPerDay;
+ this.enabled = enabled;
+ this.registered = registered;
+ setRoles(roles);
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public Date getRegistered() {
+ return registered;
+ }
+
+ public void setRegistered(Date registered) {
+ this.registered = registered;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public int getCaloriesPerDay() {
+ return caloriesPerDay;
+ }
+
+ public void setCaloriesPerDay(int caloriesPerDay) {
+ this.caloriesPerDay = caloriesPerDay;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public Set getRoles() {
+ return roles;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setRoles(Collection roles) {
+ this.roles = CollectionUtils.isEmpty(roles) ? Collections.emptySet() : EnumSet.copyOf(roles);
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "id=" + id +
+ ", email=" + email +
+ ", name=" + name +
+ ", enabled=" + enabled +
+ ", roles=" + roles +
+ ", caloriesPerDay=" + caloriesPerDay +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java
new file mode 100644
index 000000000000..f0578ff40457
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/MealRepository.java
@@ -0,0 +1,23 @@
+package ru.javawebinar.topjava.repository;
+
+import ru.javawebinar.topjava.model.Meal;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public interface MealRepository {
+ // null if updated meal do not belong to userId
+ Meal save(Meal meal, int userId);
+
+ // false if meal do not belong to userId
+ boolean delete(int id, int userId);
+
+ // null if meal do not belong to userId
+ Meal get(int id, int userId);
+
+ // ORDERED dateTime desc
+ List getAll(int userId);
+
+ // ORDERED dateTime desc
+ List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId);
+}
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/repository/jdbc/JdbcMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepositoryImpl.java
new file mode 100644
index 000000000000..80bbdb581de1
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepositoryImpl.java
@@ -0,0 +1,87 @@
+package ru.javawebinar.topjava.repository.jdbc;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.support.DataAccessUtils;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Repository
+public class JdbcMealRepositoryImpl implements MealRepository {
+
+ private static final RowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(Meal.class);
+
+ private final JdbcTemplate jdbcTemplate;
+
+ private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+
+ private final SimpleJdbcInsert insertMeal;
+
+ @Autowired
+ public JdbcMealRepositoryImpl(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
+ this.insertMeal = new SimpleJdbcInsert(jdbcTemplate)
+ .withTableName("meals")
+ .usingGeneratedKeyColumns("id");
+
+ this.jdbcTemplate = jdbcTemplate;
+ this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
+ }
+
+ @Override
+ public Meal save(Meal meal, int userId) {
+ MapSqlParameterSource map = new MapSqlParameterSource()
+ .addValue("id", meal.getId())
+ .addValue("description", meal.getDescription())
+ .addValue("calories", meal.getCalories())
+ .addValue("date_time", meal.getDateTime())
+ .addValue("user_id", userId);
+
+ if (meal.isNew()) {
+ Number newId = insertMeal.executeAndReturnKey(map);
+ meal.setId(newId.intValue());
+ } else {
+ if (namedParameterJdbcTemplate.update("" +
+ "UPDATE meals " +
+ " SET description=:description, calories=:calories, date_time=:date_time " +
+ " WHERE id=:id AND user_id=:user_id"
+ , map) == 0) {
+ return null;
+ }
+ }
+ return meal;
+ }
+
+ @Override
+ public boolean delete(int id, int userId) {
+ return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ List meals = jdbcTemplate.query(
+ "SELECT * FROM meals WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId);
+ return DataAccessUtils.singleResult(meals);
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return jdbcTemplate.query(
+ "SELECT * FROM meals WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId);
+ }
+
+ @Override
+ public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) {
+ return jdbcTemplate.query(
+ "SELECT * FROM meals WHERE user_id=? AND date_time BETWEEN ? AND ? ORDER BY date_time DESC",
+ ROW_MAPPER, userId, startDate, endDate);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java
new file mode 100644
index 000000000000..fde6581c6be4
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java
@@ -0,0 +1,74 @@
+package ru.javawebinar.topjava.repository.jdbc;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.support.DataAccessUtils;
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
+import org.springframework.stereotype.Repository;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.repository.UserRepository;
+
+import java.util.List;
+
+@Repository
+public class JdbcUserRepositoryImpl implements UserRepository {
+
+ private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class);
+
+ private final JdbcTemplate jdbcTemplate;
+
+ private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+
+ private final SimpleJdbcInsert insertUser;
+
+ @Autowired
+ public JdbcUserRepositoryImpl(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
+ this.insertUser = new SimpleJdbcInsert(jdbcTemplate)
+ .withTableName("users")
+ .usingGeneratedKeyColumns("id");
+
+ this.jdbcTemplate = jdbcTemplate;
+ this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
+ }
+
+ @Override
+ public User save(User user) {
+ BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user);
+
+ if (user.isNew()) {
+ Number newKey = insertUser.executeAndReturnKey(parameterSource);
+ user.setId(newKey.intValue());
+ } else if (namedParameterJdbcTemplate.update(
+ "UPDATE users SET name=:name, email=:email, password=:password, " +
+ "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", parameterSource) == 0) {
+ return null;
+ }
+ return user;
+ }
+
+ @Override
+ public boolean delete(int id) {
+ return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0;
+ }
+
+ @Override
+ public User get(int id) {
+ List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id);
+ return DataAccessUtils.singleResult(users);
+ }
+
+ @Override
+ public User getByEmail(String email) {
+// return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email);
+ List users = jdbcTemplate.query("SELECT * FROM users WHERE email=?", ROW_MAPPER, email);
+ return DataAccessUtils.singleResult(users);
+ }
+
+ @Override
+ public List getAll() {
+ return jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java
new file mode 100644
index 000000000000..31e36c5d291c
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java
@@ -0,0 +1,27 @@
+package ru.javawebinar.topjava.service;
+
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+public interface MealService {
+ Meal get(int id, int userId) throws NotFoundException;
+
+ void delete(int id, int userId) throws NotFoundException;
+
+ default List getBetweenDates(LocalDate startDate, LocalDate endDate, int userId) {
+ return getBetweenDateTimes(LocalDateTime.of(startDate, LocalTime.MIN), LocalDateTime.of(endDate, LocalTime.MAX), userId);
+ }
+
+ List getBetweenDateTimes(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId);
+
+ List getAll(int userId);
+
+ void update(Meal meal, int userId) throws NotFoundException;
+
+ Meal create(Meal meal, int userId);
+}
\ 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..b083eaf84c74
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/MealServiceImpl.java
@@ -0,0 +1,56 @@
+package ru.javawebinar.topjava.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.repository.MealRepository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId;
+
+@Service
+public class MealServiceImpl implements MealService {
+
+ private final MealRepository repository;
+
+ @Autowired
+ public MealServiceImpl(MealRepository repository) {
+ this.repository = repository;
+ }
+
+ @Override
+ public Meal get(int id, int userId) {
+ return checkNotFoundWithId(repository.get(id, userId), id);
+ }
+
+ @Override
+ public void delete(int id, int userId) {
+ checkNotFoundWithId(repository.delete(id, userId), id);
+ }
+
+ @Override
+ public List getBetweenDateTimes(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) {
+ Assert.notNull(startDateTime, "startDateTime must not be null");
+ Assert.notNull(endDateTime, "endDateTime must not be null");
+ return repository.getBetween(startDateTime, endDateTime, userId);
+ }
+
+ @Override
+ public List getAll(int userId) {
+ return repository.getAll(userId);
+ }
+
+ @Override
+ public void update(Meal meal, int userId) {
+ checkNotFoundWithId(repository.save(meal, userId), meal.getId());
+ }
+
+ @Override
+ public Meal create(Meal meal, int userId) {
+ Assert.notNull(meal, "meal must not be null");
+ return repository.save(meal, userId);
+ }
+}
\ 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..a3f090177a3a
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java
@@ -0,0 +1,57 @@
+package ru.javawebinar.topjava.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import ru.javawebinar.topjava.model.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;
+
+@Service
+public class UserServiceImpl implements UserService {
+
+ private final UserRepository repository;
+
+ @Autowired
+ public UserServiceImpl(UserRepository repository) {
+ this.repository = repository;
+ }
+
+ @Override
+ public User create(User user) {
+ Assert.notNull(user, "user must not be null");
+ 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 {
+ Assert.notNull(email, "email must not be null");
+ return checkNotFound(repository.getByEmail(email), "email=" + email);
+ }
+
+ @Override
+ public List getAll() {
+ return repository.getAll();
+ }
+
+ @Override
+ public void update(User user) {
+ Assert.notNull(user, "user must not be null");
+ checkNotFoundWithId(repository.save(user), user.getId());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java
new file mode 100644
index 000000000000..eedce0f2f75c
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java
@@ -0,0 +1,54 @@
+package ru.javawebinar.topjava.to;
+
+import java.time.LocalDateTime;
+
+public class MealTo {
+ private final Integer id;
+
+ private final LocalDateTime dateTime;
+
+ private final String description;
+
+ private final int calories;
+
+ private final boolean excess;
+
+ public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) {
+ this.id = id;
+ this.dateTime = dateTime;
+ this.description = description;
+ this.calories = calories;
+ this.excess = excess;
+ }
+
+ public Integer getId() {
+ return id;
+ }
+
+ public LocalDateTime getDateTime() {
+ return dateTime;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public boolean isExcess() {
+ return excess;
+ }
+
+ @Override
+ public String toString() {
+ return "MealTo{" +
+ "id=" + id +
+ ", dateTime=" + dateTime +
+ ", description='" + description + '\'' +
+ ", calories=" + calories +
+ ", excess=" + excess +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
new file mode 100644
index 000000000000..a1b5bfda4928
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java
@@ -0,0 +1,31 @@
+package ru.javawebinar.topjava.util;
+
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+public class DateTimeUtil {
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+
+ // DataBase doesn't support LocalDate.MIN/MAX
+ public static final LocalDate MIN_DATE = LocalDate.of(1, 1, 1);
+ public static final LocalDate MAX_DATE = LocalDate.of(3000, 1, 1);
+
+ private DateTimeUtil() {
+ }
+
+ public static String toString(LocalDateTime ldt) {
+ return ldt == null ? "" : ldt.format(DATE_TIME_FORMATTER);
+ }
+
+ public static LocalDate parseLocalDate(String str) {
+ return StringUtils.isEmpty(str) ? null : LocalDate.parse(str);
+ }
+
+ public static LocalTime parseLocalTime(String str) {
+ return StringUtils.isEmpty(str) ? null : LocalTime.parse(str);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java
new file mode 100644
index 000000000000..aa96354ba773
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java
@@ -0,0 +1,47 @@
+package ru.javawebinar.topjava.util;
+
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.to.MealTo;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static java.util.stream.Collectors.toList;
+
+public class MealsUtil {
+
+ public static final int DEFAULT_CALORIES_PER_DAY = 2000;
+
+ private MealsUtil() {
+ }
+
+ public static List getWithExcess(Collection meals, int caloriesPerDay) {
+ return getFilteredWithExcess(meals, caloriesPerDay, meal -> true);
+ }
+
+ public static List getFilteredWithExcess(Collection meals, int caloriesPerDay, LocalTime startTime, LocalTime endTime) {
+ return getFilteredWithExcess(meals, caloriesPerDay, meal -> Util.isBetween(meal.getTime(), startTime, endTime));
+ }
+
+ private static List getFilteredWithExcess(Collection meals, int caloriesPerDay, Predicate filter) {
+ Map caloriesSumByDate = meals.stream()
+ .collect(
+ Collectors.groupingBy(Meal::getDate, Collectors.summingInt(Meal::getCalories))
+// Collectors.toMap(Meal::getDate, Meal::getCalories, Integer::sum)
+ );
+
+ return meals.stream()
+ .filter(filter)
+ .map(meal -> createWithExcess(meal, caloriesSumByDate.get(meal.getDate()) > caloriesPerDay))
+ .collect(toList());
+ }
+
+ public static MealTo createWithExcess(Meal meal, boolean Excess) {
+ return new MealTo(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), Excess);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/util/Util.java b/src/main/java/ru/javawebinar/topjava/util/Util.java
new file mode 100644
index 000000000000..22d3896971e2
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/Util.java
@@ -0,0 +1,15 @@
+package ru.javawebinar.topjava.util;
+
+public class Util {
+
+ private Util() {
+ }
+
+ public static > boolean isBetween(T value, T start, T end) {
+ return value.compareTo(start) >= 0 && value.compareTo(end) <= 0;
+ }
+
+ public static T orElse(T value, T defaultValue) {
+ return value == null ? defaultValue : value;
+ }
+}
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..536408bb59c0
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java
@@ -0,0 +1,45 @@
+package ru.javawebinar.topjava.util;
+
+
+import ru.javawebinar.topjava.model.AbstractBaseEntity;
+import ru.javawebinar.topjava.util.exception.NotFoundException;
+
+public class ValidationUtil {
+
+ public static T checkNotFoundWithId(T object, int id) {
+ return checkNotFound(object, "id=" + id);
+ }
+
+ private ValidationUtil() {
+ }
+
+ public static void checkNotFoundWithId(boolean found, int id) {
+ checkNotFound(found, "id=" + id);
+ }
+
+ public static T checkNotFound(T object, String msg) {
+ checkNotFound(object != null, msg);
+ return object;
+ }
+
+ public static void checkNotFound(boolean found, String msg) {
+ if (!found) {
+ throw new NotFoundException("Not found entity with " + msg);
+ }
+ }
+
+ public static void checkNew(AbstractBaseEntity entity) {
+ if (!entity.isNew()) {
+ throw new IllegalArgumentException(entity + " must be new (id=null)");
+ }
+ }
+
+ public static void assureIdConsistent(AbstractBaseEntity entity, int id) {
+// 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/MealServlet.java b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java
new file mode 100644
index 000000000000..6e936f48a34e
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/MealServlet.java
@@ -0,0 +1,98 @@
+package ru.javawebinar.topjava.web;
+
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.web.meal.MealRestController;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Objects;
+
+import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate;
+import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime;
+
+public class MealServlet extends HttpServlet {
+
+ private ConfigurableApplicationContext springContext;
+ private MealRestController mealController;
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ super.init(config);
+ springContext = new ClassPathXmlApplicationContext("spring/spring-app.xml", "spring/spring-db.xml");
+ mealController = springContext.getBean(MealRestController.class);
+ }
+
+ @Override
+ public void destroy() {
+ springContext.close();
+ super.destroy();
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ request.setCharacterEncoding("UTF-8");
+ String action = request.getParameter("action");
+ if (action == null) {
+ Meal meal = new Meal(
+ LocalDateTime.parse(request.getParameter("dateTime")),
+ request.getParameter("description"),
+ Integer.parseInt(request.getParameter("calories")));
+
+ if (request.getParameter("id").isEmpty()) {
+ mealController.create(meal);
+ } else {
+ mealController.update(meal, getId(request));
+ }
+ response.sendRedirect("meals");
+
+ } else if ("filter".equals(action)) {
+ LocalDate startDate = parseLocalDate(request.getParameter("startDate"));
+ LocalDate endDate = parseLocalDate(request.getParameter("endDate"));
+ LocalTime startTime = parseLocalTime(request.getParameter("startTime"));
+ LocalTime endTime = parseLocalTime(request.getParameter("endTime"));
+ request.setAttribute("meals", mealController.getBetween(startDate, startTime, endDate, endTime));
+ request.getRequestDispatcher("/meals.jsp").forward(request, response);
+ }
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ String action = request.getParameter("action");
+
+ switch (action == null ? "all" : action) {
+ case "delete":
+ int id = getId(request);
+ mealController.delete(id);
+ response.sendRedirect("meals");
+ break;
+ case "create":
+ case "update":
+ final Meal meal = "create".equals(action) ?
+ new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000) :
+ mealController.get(getId(request));
+ request.setAttribute("meal", meal);
+ request.getRequestDispatcher("/mealForm.jsp").forward(request, response);
+ break;
+ case "all":
+ default:
+ request.setAttribute("meals", mealController.getAll());
+ request.getRequestDispatcher("/meals.jsp").forward(request, response);
+ break;
+ }
+ }
+
+ private int getId(HttpServletRequest request) {
+ String paramId = Objects.requireNonNull(request.getParameter("id"));
+ return Integer.parseInt(paramId);
+ }
+}
diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java
new file mode 100644
index 000000000000..4bad5863e3c6
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java
@@ -0,0 +1,25 @@
+package ru.javawebinar.topjava.web;
+
+import ru.javawebinar.topjava.model.AbstractBaseEntity;
+
+import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY;
+
+public class SecurityUtil {
+
+ private static int id = AbstractBaseEntity.START_SEQ;
+
+ private SecurityUtil() {
+ }
+
+ public static int authUserId() {
+ return id;
+ }
+
+ public static void setAuthUserId(int id) {
+ SecurityUtil.id = id;
+ }
+
+ public static int authUserCaloriesPerDay() {
+ return DEFAULT_CALORIES_PER_DAY;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/UserServlet.java b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java
new file mode 100644
index 000000000000..226023400c70
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/UserServlet.java
@@ -0,0 +1,28 @@
+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 doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ int userId = Integer.parseInt(request.getParameter("userId"));
+ SecurityUtil.setAuthUserId(userId);
+ response.sendRedirect("meals");
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ log.debug("forward to users");
+ request.getRequestDispatcher("/users.jsp").forward(request, response);
+ }
+}
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..0c5d07e464f8
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java
@@ -0,0 +1,82 @@
+package ru.javawebinar.topjava.web.meal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import ru.javawebinar.topjava.model.Meal;
+import ru.javawebinar.topjava.service.MealService;
+import ru.javawebinar.topjava.to.MealTo;
+import ru.javawebinar.topjava.util.DateTimeUtil;
+import ru.javawebinar.topjava.util.MealsUtil;
+import ru.javawebinar.topjava.web.SecurityUtil;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.Util.orElse;
+import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNew;
+
+@Controller
+public class MealRestController {
+ private static final Logger log = LoggerFactory.getLogger(MealRestController.class);
+
+ private final MealService service;
+
+ @Autowired
+ public MealRestController(MealService service) {
+ this.service = service;
+ }
+
+ public Meal get(int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("get meal {} for user {}", id, userId);
+ return service.get(id, userId);
+ }
+
+ public void delete(int id) {
+ int userId = SecurityUtil.authUserId();
+ log.info("delete meal {} for user {}", id, userId);
+ service.delete(id, userId);
+ }
+
+ public List getAll() {
+ int userId = SecurityUtil.authUserId();
+ log.info("getAll for user {}", userId);
+ return MealsUtil.getWithExcess(service.getAll(userId), SecurityUtil.authUserCaloriesPerDay());
+ }
+
+ public Meal create(Meal meal) {
+ int userId = SecurityUtil.authUserId();
+ checkNew(meal);
+ log.info("create {} for user {}", meal, userId);
+ return service.create(meal, userId);
+ }
+
+ public void update(Meal meal, int id) {
+ int userId = SecurityUtil.authUserId();
+ assureIdConsistent(meal, id);
+ log.info("update {} for user {}", meal, userId);
+ service.update(meal, userId);
+ }
+
+ /**
+ * Filter separately
+ *
by date
+ *
by time for every date
+ *
+ */
+ public List getBetween(LocalDate startDate, LocalTime startTime, LocalDate endDate, LocalTime endTime) {
+ int userId = SecurityUtil.authUserId();
+ log.info("getBetween dates({} - {}) time({} - {}) for user {}", startDate, endDate, startTime, endTime, userId);
+
+ List mealsDateFiltered = service.getBetweenDates(
+ orElse(startDate, DateTimeUtil.MIN_DATE), orElse(endDate, DateTimeUtil.MAX_DATE), userId);
+
+ return MealsUtil.getFilteredWithExcess(mealsDateFiltered, SecurityUtil.authUserCaloriesPerDay(),
+ orElse(startTime, LocalTime.MIN), orElse(endTime, LocalTime.MAX)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
new file mode 100644
index 000000000000..0000f1c1e02f
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java
@@ -0,0 +1,51 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import ru.javawebinar.topjava.model.User;
+import ru.javawebinar.topjava.service.UserService;
+
+import java.util.List;
+
+import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent;
+import static ru.javawebinar.topjava.util.ValidationUtil.checkNew;
+
+public abstract class AbstractUserController {
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ @Autowired
+ private UserService service;
+
+ public List getAll() {
+ log.info("getAll");
+ return service.getAll();
+ }
+
+ public User get(int id) {
+ log.info("get {}", id);
+ return service.get(id);
+ }
+
+ public User create(User user) {
+ log.info("create {}", user);
+ checkNew(user);
+ return service.create(user);
+ }
+
+ public void delete(int id) {
+ log.info("delete {}", id);
+ service.delete(id);
+ }
+
+ public void update(User user, int id) {
+ log.info("update {} with id={}", user, id);
+ assureIdConsistent(user, id);
+ service.update(user);
+ }
+
+ public User getByMail(String email) {
+ log.info("getByEmail {}", email);
+ return service.getByEmail(email);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
new file mode 100644
index 000000000000..b37a8ed6c8a5
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java
@@ -0,0 +1,40 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.stereotype.Controller;
+import ru.javawebinar.topjava.model.User;
+
+import java.util.List;
+
+@Controller
+public class AdminRestController extends AbstractUserController {
+
+ @Override
+ public List getAll() {
+ return super.getAll();
+ }
+
+ @Override
+ public User get(int id) {
+ return super.get(id);
+ }
+
+ @Override
+ public User create(User user) {
+ return super.create(user);
+ }
+
+ @Override
+ public void delete(int id) {
+ super.delete(id);
+ }
+
+ @Override
+ public void update(User user, int id) {
+ super.update(user, id);
+ }
+
+ @Override
+ public User getByMail(String email) {
+ return super.getByMail(email);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
new file mode 100644
index 000000000000..7d3702c31c46
--- /dev/null
+++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java
@@ -0,0 +1,22 @@
+package ru.javawebinar.topjava.web.user;
+
+import org.springframework.stereotype.Controller;
+import ru.javawebinar.topjava.model.User;
+
+import static ru.javawebinar.topjava.web.SecurityUtil.authUserId;
+
+@Controller
+public class ProfileRestController extends AbstractUserController {
+
+ public User get() {
+ return super.get(authUserId());
+ }
+
+ public void delete() {
+ super.delete(authUserId());
+ }
+
+ public void update(User user) {
+ super.update(user, authUserId());
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql
new file mode 100644
index 000000000000..f87f5b274e85
--- /dev/null
+++ b/src/main/resources/db/initDB.sql
@@ -0,0 +1,37 @@
+DROP TABLE IF EXISTS user_roles;
+DROP TABLE IF EXISTS meals;
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS global_seq;
+
+CREATE SEQUENCE global_seq START 100000;
+
+CREATE TABLE users
+(
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ name VARCHAR NOT NULL,
+ email VARCHAR NOT NULL,
+ password VARCHAR NOT NULL,
+ registered TIMESTAMP DEFAULT now() NOT NULL,
+ enabled BOOL DEFAULT TRUE NOT NULL,
+ calories_per_day INTEGER DEFAULT 2000 NOT NULL
+);
+CREATE UNIQUE INDEX users_unique_email_idx ON users (email);
+
+CREATE TABLE user_roles
+(
+ user_id INTEGER NOT NULL,
+ role VARCHAR,
+ CONSTRAINT user_roles_idx UNIQUE (user_id, role),
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+
+CREATE TABLE meals (
+ id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
+ user_id INTEGER NOT NULL,
+ date_time TIMESTAMP NOT NULL,
+ description TEXT NOT NULL,
+ calories INT NOT NULL,
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX meals_unique_user_datetime_idx
+ ON meals (user_id, date_time);
\ No newline at end of file
diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql
new file mode 100644
index 000000000000..29a97efdc2b8
--- /dev/null
+++ b/src/main/resources/db/populateDB.sql
@@ -0,0 +1,22 @@
+DELETE FROM user_roles;
+DELETE FROM meals;
+DELETE FROM users;
+ALTER SEQUENCE global_seq RESTART WITH 100000;
+
+INSERT INTO users (name, email, password) VALUES
+ ('User', 'user@yandex.ru', 'password'),
+ ('Admin', 'admin@gmail.com', 'admin');
+
+INSERT INTO user_roles (role, user_id) VALUES
+ ('ROLE_USER', 100000),
+ ('ROLE_ADMIN', 100001);
+
+INSERT INTO meals (date_time, description, calories, user_id)
+VALUES ('2015-05-30 10:00:00', 'Завтрак', 500, 100000),
+ ('2015-05-30 13:00:00', 'Обед', 1000, 100000),
+ ('2015-05-30 20:00:00', 'Ужин', 500, 100000),
+ ('2015-05-31 10:00:00', 'Завтрак', 500, 100000),
+ ('2015-05-31 13:00:00', 'Обед', 1000, 100000),
+ ('2015-05-31 20:00:00', 'Ужин', 510, 100000),
+ ('2015-06-01 14:00:00', 'Админ ланч', 510, 100001),
+ ('2015-06-01 21:00:00', 'Админ ужин', 1500, 100001);
diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties
new file mode 100644
index 000000000000..9942a03036fe
--- /dev/null
+++ b/src/main/resources/db/postgres.properties
@@ -0,0 +1,14 @@
+#database.url=jdbc:postgresql://ec2-54-247-74-197.eu-west-1.compute.amazonaws.com:5432/de4fjsqhdvl7ld?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory
+#database.username=anbxkjtzukqacj
+#database.password=da1f25b2a38784fb0d46858e5b8fc168e08c9e1e9c72faea5bbac9c0e1f9c24f
+
+database.url=jdbc:postgresql://localhost:5432/topjava
+database.username=user
+database.password=password
+database.driverClassName=org.postgresql.Driver
+
+database.init=true
+jdbc.initLocation=initDB.sql
+jpa.showSql=true
+hibernate.format_sql=true
+hibernate.use_sql_comments=true
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 000000000000..c7bffc3a958c
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ ${TOPJAVA_ROOT}/log/topjava.log
+
+
+ UTF-8
+ %date %-5level %logger{50}.%M:%L - %msg%n
+
+
+
+
+
+ UTF-8
+ %d{HH:mm:ss.SSS} %-5level %class{50}.%M:%L - %msg%n
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml
new file mode 100644
index 000000000000..4b239a370a1b
--- /dev/null
+++ b/src/main/resources/spring/spring-app.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml
new file mode 100644
index 000000000000..ef29e6a69d0f
--- /dev/null
+++ b/src/main/resources/spring/spring-db.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/WEB-INF/tld/functions.tld b/src/main/webapp/WEB-INF/tld/functions.tld
new file mode 100644
index 000000000000..d138fecdbfb5
--- /dev/null
+++ b/src/main/webapp/WEB-INF/tld/functions.tld
@@ -0,0 +1,16 @@
+
+
+
+ 1.0
+ functions
+ http://topjava.javawebinar.ru/functions
+
+
+ formatDateTime
+ ru.javawebinar.topjava.util.DateTimeUtil
+ java.lang.String toString(java.time.LocalDateTime)
+
+
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000000..bd98d3bf3f6a
--- /dev/null
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,29 @@
+
+
+ Topjava
+
+
+ userServlet
+ ru.javawebinar.topjava.web.UserServlet
+ 0
+
+
+ userServlet
+ /users
+
+
+
+ mealServlet
+ ru.javawebinar.topjava.web.MealServlet
+ 0
+
+
+ mealServlet
+ /meals
+
+
+
diff --git a/src/main/webapp/css/style.css b/src/main/webapp/css/style.css
new file mode 100644
index 000000000000..26a14ce43e6d
--- /dev/null
+++ b/src/main/webapp/css/style.css
@@ -0,0 +1,24 @@
+dl {
+ background: none repeat scroll 0 0 #FAFAFA;
+ margin: 8px 0;
+ padding: 0;
+}
+
+dt {
+ display: inline-block;
+ width: 170px;
+}
+
+dd {
+ display: inline-block;
+ margin-left: 8px;
+ vertical-align: top;
+}
+
+tr[data-mealExcess="false"] {
+ color: green;
+}
+
+tr[data-mealExcess="true"] {
+ color: red;
+}
diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html
new file mode 100644
index 000000000000..886449733a86
--- /dev/null
+++ b/src/main/webapp/index.html
@@ -0,0 +1,18 @@
+
+
+
+ Java Enterprise (Topjava)
+
+
+