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/README.md b/README.md new file mode 100644 index 000000000000..9dafd9ca7ccb --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +Java Enterprise Online Project +=============================== +Наиболее востребованные технологии /инструменты / фреймворки Java Enterprise: +Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + plugins. + +**Старт проекта 10.09.2015** + +Вводное занятие +=============== + +## Описание проекта + +#### Демо разрабатываемого приложения + +## Системы управления версиями. Git. + +- Система управления версиями. VCS/DVSC. +- Ресурсы: + - Интерактивная Git обучалка + - Основы Git + - Working with remote repositories + - Видео по обучению Git + - Как начать работать с GitHub: быстрый старт + - Справочник в графическом виде + - Git Overview + - Видеокурс по Git + +## Работа с проектом (выполнять инструкции) +- **Prepare_ to_ HW0.patch (скачать и положить в каталог вашего проекта)** +- Доступно о Java 8 Lambda + +## Инструкция по шагам (из видео): +- Установить ПО (git, JDK8, IntelliJ IDEA, Maven) +- Создать аккаунт на GitHub +- Сделать Fork ЭТОГО проекта (https://github.com/JavaOPs/topjava) +- Сделать локальный репозиторий проекта: +
git clone https://github.com/[Ваш аккаунт]/topjava.git
+- Открыть и настроить проект в IDEA +- По ходу видео сделать Apply Patch... скаченного патча Prepare_ to_ HW0.patch +- Закоммитить и запушить изменения (commit + push) +- Сделать ветку домашнего задания +- Выполнить задание и залить на GitHub (commit + push) +- Отослать в skype мне ссылку вашей ветки GitHub (при оплате расширенного участия) +- Переключиться в основную ветку проекта master. + +### Вы готовы к участию в проекте! + +- Ресурсы + - Java 8: Lambda выражения + - Java 8 Date and Time API + - Java 8: Потоки + - Pуководство по Java 8 Stream + - Лямбда-выражения в Java 8 + +## Домашнее задание HW0 + Реализовать UserMealsUtil.getFilteredMealsWithExceeded: + - должны возвращаться только записи между startTime и endTime + - поле UserMealWithExceed.exceed должно показывать, + превышает ли сумма калорий за весь день параметра метода caloriesPerDay + + Т.е UserMealWithExceed - это запись одной еды, но поле exceeded + будет одинаково для всех записей за этот день. + +Optional + + Сделать реализация через Java 8 Stream API diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml new file mode 100644 index 000000000000..9b01a3cabab9 --- /dev/null +++ b/config/Topjava-soapui-project.xml @@ -0,0 +1,19 @@ + +http://localhost:8080text/html;charset=utf-8500 401 403htmlapplication/json200ns:Response0dataapplication/json;charset=UTF-8200Response<xml-fragment/>http://localhost:8080http://localhost/topjava/rest/admin/usersadmin@gmail.comadminBasicBasicGlobal HTTP Settingsapplication/jsontext/html;charset=utf-8500htmlapplication/json201user:Responseapplication/json;charset=UTF-8201user:Response<xml-fragment/>http://localhost:8080{ +http://localhost:8080text/html;charset=utf-8500 401htmlapplication/json200ns:Response<xml-fragment/>http://localhost:8080http://localhost/topjava/rest/admin/usersuser@yandex.rupasswordNo AuthorizationBasicNo Authorizationapplication/jsontext/html;charset=utf-8500htmlapplication/json201user:Response<xml-fragment/>http://localhost:8080{ + "name": "New2", + "email": "new2@yandex.ru", + "password": "passwordNew", + "roles": ["ROLE_USER"] +}http://localhost/topjava/rest/admin/usersadmin@gmail.comadminBasicBasicGlobal HTTP Settingstext/html;charset=utf-8401html<xml-fragment/>http://localhost:8080http://localhost/topjava/rest/admin/usersNo Authorizationtext/html;charset=utf-8500htmlapplication/json200ns:Responseapplication/json;charset=UTF-8200ns:Response<xml-fragment/>http://localhost:8080http://localhost/topjava/rest/admin/users/100000No AuthorizationBasicNo Authorizationtext/html;charset=utf-8405 500htmlapplication/json200data200data200data200data<xml-fragment/>http://localhost:8080{ +}http://localhost/topjava/rest/admin/usersNo AuthorizationBasicNo Authorizationtext/html;charset=utf-8500htmlapplication/json200ns:Response<xml-fragment/>http://localhost:8080http://localhost/topjava/rest/admin/users/100000No AuthorizationBasicNo Authorizationtext/html;charset=utf-8405 500htmlapplication/json200data200data200data<xml-fragment/>http://localhost:8080{ + "name": "UserUpdated", + "email": "user@yandex.ru", + "password": "passwordNew", + "roles": ["ROLE_USER"] +}http://localhost/topjava/rest/admin/users/100000BasicBasicGlobal HTTP Settingstext/html;charset=utf-8500htmlapplication/json200ns:Response<xml-fragment/>http://localhost:8080http://localhost/topjava/rest/profileBasicBasicGlobal HTTP Settingsapplication/jsontext/html;charset=utf-8500 405htmlapplication/json201user:Response200data200data200data200data200data<xml-fragment/>http://localhost:8080{ + "name": "New777", + "email": "new777@yandex.ru", + "password": "passwordNew", + "roles": ["ROLE_USER"] +}http://localhost/topjava/rest/profileNo AuthorizationBasicNo Authorization200datatext/html;charset=utf-8500htmlapplication/json200data<xml-fragment/>http://localhost:8080http://localhost/topjava/rest/profileNo AuthorizationBasicNo Authorization \ No newline at end of file diff --git a/config/messages/app.properties b/config/messages/app.properties new file mode 100644 index 000000000000..b00e6b92ca99 --- /dev/null +++ b/config/messages/app.properties @@ -0,0 +1,15 @@ +app.title=Calories management +app.footer=Webinar Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery application +app.login=Login +app.logout=Logout +app.registered=You are registered. Please Sign in. + +users.title=User list +users.edit=Edit user +users.add=Add user + +meals.title=Meals list +meals.edit=Edit meal +meals.add=Add meal + +common.select=Select \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties new file mode 100644 index 000000000000..f0562f8e034e --- /dev/null +++ b/config/messages/app_ru.properties @@ -0,0 +1,15 @@ +app.title=Подсчет калорий +app.footer=Приложение по вебинару Maven/ Spring/ Security/ JPA(Hibernate)/ Jackson/jQuery +app.login=Войти +app.logout=Выйти +app.registered=Вы загеристрированы. Введите ваш логин/пароль. + +users.title=Список пользователей +users.edit=Радактировать пользователя +users.add=Добавить пользователя + +meals.title=Список еды +meals.edit=Редактировать еду +meals.add=Добавить еду + +common.select=Выбрать diff --git a/config/tomcat/setenv.bat b/config/tomcat/setenv.bat new file mode 100644 index 000000000000..ebbd659386f6 --- /dev/null +++ b/config/tomcat/setenv.bat @@ -0,0 +1,4 @@ +rem run tomcat with JMX ability +rem Run Tomcat as admin +rem for remote connection add -Djava.rmi.server.hostname=TomcatServer_IP +set CATALINA_OPTS=-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false diff --git a/config/topjava.wadl b/config/topjava.wadl new file mode 100644 index 000000000000..bad740ab4c00 --- /dev/null +++ b/config/topjava.wadl @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/description.md b/description.md new file mode 100644 index 000000000000..c58f903605d4 --- /dev/null +++ b/description.md @@ -0,0 +1,64 @@ +#### Разработка полнофункционального Spring/JPA Enterprise приложения c авторизацией и правами доступа на основе ролей используя наиболее популярные инструменты и технологии Java: Maven, Spring, Security, JPA(Hibernate), REST(Jackson), Bootstrap, jQuery + plugins +- Основное внимание будет уделяться способам решения многочисленных проблем разработки в Spring/JPA, а также структурному (красивому и надежному) java кодированию и архитектуре приложения. +- Каждая итерация проекта в закрепляется домашним заданием по реализации схожей функциональности. Следующее занятие начинается с разбора домашних заданий. +- Большое внимание уделяется тестированию кода: в проекте ~ 85 JUnit тестов. +- Несмотря на относительно небольшой размер, приложение разрабатывается с нуля как большой проект (например мы используем кэш 2-го уровня Hibernate, настраиваем Jenkins для работы с ленивой загрузкой +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 +- Spring кэш +- Кэш Hibernate + +## Разработка WEB +- Spring Web +- JPS, JSTL, i18n +- Tomcat maven plugin. JNDI +- Spring Web MVC +- Spring Internationalization +- Тестирование Spring MVC +- REST контроллеры +- Тестирование REST контроллеров. Jackson. +- jackson-datatype-hibernate. Тестирование через матчеры. +- Тестирование через SoapUi. UTF-8 +- WebJars. Dandelion bundles +- Bootstrap. Datatables. +- AJAX. jQuery. Notifications. +- Spring Security +- Spring Binding/Validation +- Работа с Datatables через Ajax. +- Spring Security Test +- Encoding password +- CSRF +- form-login. Spring Security Taglib +- Handler interceptor +- Spring Exception Handling +- Деплой в Heroku diff --git a/doc/lesson01.md b/doc/lesson01.md new file mode 100644 index 000000000000..e9999c2b68b6 --- /dev/null +++ b/doc/lesson01.md @@ -0,0 +1,84 @@ +# Занятие 1 онлайн проекта Topjava + +**Напоминаю, что патчи в материалах урока необходимо накатывать (Apply Patch) на ветку MASTER и БЕЗ ВАШИХ ИЗМЕНЕНИЙ, иначе с ними будут проблемы. Делать Apply можно как по ходу видео, так и немного заранее - тогда при просмотре видео вы сразу сможете отслеживать изменения кода проекта.** +## Материалы урока + +## Домашнее задание: реализация getFilteredMealsWithExceeded через Stream API +- Доступно о Java 8 Lambda +- **1_ Lambda_ samples.patch** +- **2_ HW0.patch** +- Перевод "Java 8 Stream Tutorial" +- Основное в Java 8 + +## Работа с git в IDEA. HW0: реализация через цикл. +- **3_ HW0_ cycles.patch** + +## Обзор используемых в проекте технологий. Интеграция ПО. +- Обзор популярности инструментов и технологий Java за 2014 г. +- Видео "Приложение Spring Pet Clinic" +- Приложение Spring Pet Clinic. +- Demo Spring Pet Clinic + +## Maven. +- Home Page +- Среда сборки проектов Maven. +- The Central Repository +- Maven archetype. Создание проекта на основе maven-archetype-webapp. Сборка проекта. +- Настройка пропертей Maven: кодировка, java version, зависимости, maven-compiler-plugin +- Build Lifecycle. +- Dependency Mechanism +- Зависимости, профили +- The Reactor. Snapshots +- Недостатки Maven. Другие инструменты сборки. +- Ресурсы: + - Maven: The Complete Reference + - Автоматизация сборки проекта + - Repository management Nexus + - Bintray: gateway to Maven Central + +## WAR. Веб-контейнер Tomcat. Сервлеты. +- **4_ Switch_ to_ war_ packaging.patch** +- **5_ Add_ servlet_ jsp_ html.patch** +- **6_ Forward_ to_ redirect.patch** + +- Перевод проекта на Web. +- Tomcat Home Page +- Сервлеты. +- Настройка и деплой в Tomcat. Tomcat manager. Remote debug. +- Запуск Tomcat через tomcat7-maven-plugin. Конфигурирование плагина. +- Запуск Tomcat из IDEA. Динамическое обновление без передеплоя. +- Redirect, Forward, Application context, Servlet context + - Томкат менеджер: http://localhost:8080/manager + - Наше приложение: http://localhost:8080/topjava + - Наш сервлет: http://localhost:8080/topjava/users + +- Ресурсы: + - Технологии Java для разработки веб-приложений + +## Логирование. +- **7_ Add_ logging.patch** + +- Видео с TopJava01 +- Java Logging: история кошмара +- Ведение лога приложения +- Добавление зависимостей логирования в проект. +- Log4j, Logback +- Конфигурирование логирования. Создание обертки логирования. Настройка Live Template. +- Интеграция IDEA с Tomcat, деплой. +- Тестирование логирования в сервлете. Переменная окружения TOPJAVA_ROOT. +- Управление логированием по JMX. +- Контекст приложения. Деплой в Tomcat без IDE. Remote debug. + +## Уровни и зависимости логгирования, JMX + +## Домашнее задание HW01 + + Попробовать разные деплои в Tomcat, remote и local debug + По аналогии с Users добавить MealServlet и mealList.jsp. + Добавить в JSP и index.html ссылки друг на друга + Задеплоить приложение (war) в Tomcat c applicationContext=topjava и проверить работу ссылок + +Optional + + Сделать реализацию CRUD (create/read/update/delete) для списка еды. + Цвет записи в таблице зависит от параметра isExceeded. \ No newline at end of file diff --git a/hr.bat b/hr.bat new file mode 100644 index 000000000000..20dab9204022 --- /dev/null +++ b/hr.bat @@ -0,0 +1,2 @@ +call mvn -B -s settings.xml -DskipTests=true clean install +call java -Dspring.profiles.active="datajpa,heroku" -DDATABASE_URL="postgres://user:password@localhost:5432/topjava" -jar target/dependency/webapp-runner.jar target/*.war diff --git a/pom.xml b/pom.xml index c8a1c78f3b29..a05259388cbe 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ ru.javawebinar topjava - jar + war 1.0-SNAPSHOT @@ -13,13 +13,46 @@ 1.8 + 7.0.57 + UTF-8 UTF-8 + + 1.9.0.RELEASE + 4.2.1.RELEASE + + 9.4-1201-jdbc41 + + + 1.1.2 + 1.7.7 + + + 4.12 + + + 5.0.2.Final + 5.1.3.Final + + 1.3 + 2.6.3 + 2.10.0 + + + 1.2 + 0.10.1 + + 3.3.5 + 2.1.4 + 2.3.4 + 2.2.4 + 4.0.2.RELEASE + 1.10.9 topjava - install + package org.apache.maven.plugins @@ -30,15 +63,300 @@ ${java.version} + + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.2 + + src/main/resources/tomcat/context.xml + + UTF-8 + tomcat,datajpa + + org.apache.coyote.http11.Http11NioProtocol + + + + org.postgresql + postgresql + ${postgres.version} + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.17 + + -Dfile.encoding=UTF-8 + + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + runtime + + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + runtime + + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + + org.springframework + spring-webmvc + ${spring.version} + + + commons-logging + commons-logging + + + + + + javax.servlet + jstl + 1.2 + + + + + org.springframework + spring-context-support + ${spring.version} + + + commons-logging + commons-logging + + + + + + + org.springframework.security + spring-security-web + ${spring-security.version} + + + org.springframework.security + spring-security-config + ${spring-security.version} + + + + + org.webjars + bootstrap + ${webjars-bootstrap.version} + + + org.webjars + jquery + ${webjars-jquery.version} + + + org.webjars + datetimepicker + ${webjars-datetimepicker.version} + + + org.webjars + noty + ${webjars-noty.version} + + + org.webjars + datatables + ${webjars-datatables.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-json.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate4 + ${jackson-json.version} + + + + org.springframework.data + spring-data-jpa + ${spring-data-jpa.version} + + + org.hibernate + hibernate-entitymanager + ${hibernate.version} + + + org.hibernate + hibernate-java8 + ${hibernate.version} + + + org.hibernate + hibernate-validator + ${hibernate-validator.version} + + + org.hibernate + hibernate-ehcache + ${hibernate.version} + + + org.springframework.security + spring-security-taglibs + ${spring-security.version} + + + + + net.sf.ehcache + ehcache + ${ehcache.version} + + + + + junit + junit + ${junit.version} + test + + + hamcrest-core + org.hamcrest + + + + + + org.hamcrest + hamcrest-all + ${hamcrest.version} + test + + + + org.springframework + spring-test + ${spring.version} + test + + + org.springframework.security + spring-security-test + ${spring-security.version} + + + hsqldb + + + org.hsqldb + hsqldb + 2.3.2 + + + + + postgres + + + org.postgresql + postgresql + ${postgres.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat.version} + + + + true + + + + heroku + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.10 + + + package + + copy + + + + + com.github.jsimone + webapp-runner + 7.0.57.2 + webapp-runner.jar + + + + + + + + + + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + diff --git a/settings.xml b/settings.xml new file mode 100644 index 000000000000..cb1c8aa69b1b --- /dev/null +++ b/settings.xml @@ -0,0 +1,11 @@ + + + + + + postgres + heroku + + diff --git a/src/main/java/ru/javawebinar/topjava/LoggedUser.java b/src/main/java/ru/javawebinar/topjava/LoggedUser.java new file mode 100644 index 000000000000..95e4f2921352 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/LoggedUser.java @@ -0,0 +1,69 @@ +package ru.javawebinar.topjava; + +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 java.util.Objects.requireNonNull; + +/** + * GKislin + * 06.03.2015. + *

+ * Mock implementation + */ +public class LoggedUser extends org.springframework.security.core.userdetails.User { + static final long serialVersionUID = 1L; + + private UserTo userTo; + + public LoggedUser(User user) { + super(user.getEmail(), + user.getPassword(), + user.isEnabled(), + user.isEnabled(), + user.isEnabled(), + user.isEnabled(), + user.getRoles()); + + this.userTo = UserUtil.asTo(user); + } + + public static LoggedUser safeGet() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null) { + return null; + } + Object user = auth.getPrincipal(); + return (user instanceof LoggedUser) ? (LoggedUser) user : null; + } + + public static LoggedUser get() { + LoggedUser user = safeGet(); + requireNonNull(user, "No authorized user found"); + return user; + } + + public UserTo getUserTo() { + return userTo; + } + + public static int id() { + return get().userTo.getId(); + } + + public static int getCaloriesPerDay() { + return get().userTo.getCaloriesPerDay(); + } + + public void update(UserTo newTo) { + userTo = newTo; + } + + @Override + public String toString() { + return userTo.toString(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/LoggerWrapper.java b/src/main/java/ru/javawebinar/topjava/LoggerWrapper.java new file mode 100644 index 000000000000..54c2c23865e2 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/LoggerWrapper.java @@ -0,0 +1,84 @@ +package ru.javawebinar.topjava; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.javawebinar.topjava.util.exception.ErrorInfo; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +/** + * User: gkislin + * Date: 22.01.14 + */ +public class LoggerWrapper { + + private Logger logger; + + public LoggerWrapper(Logger logger) { + this.logger = logger; + } + + public static LoggerWrapper get(Class aClass) { + return new LoggerWrapper(LoggerFactory.getLogger(aClass)); + } + + public void debug(String msg) { + logger.debug(msg); + } + + public void info(String msg, Object... arguments) { + logger.info(msg, arguments); + } + + public void warn(String msg) { + logger.warn(msg); + } + + public void warn(String msg, Throwable t) { + logger.warn(msg, t); + } + + public void error(String msg) { + logger.error(msg); + } + + public void error(String msg, Throwable t) { + logger.error(msg, t); + } + + public boolean isDebug() { + return logger.isDebugEnabled(); + } + + public IllegalStateException getIllegalStateException(String msg) { + return getIllegalStateException(msg, null); + } + + public IllegalStateException getIllegalStateException(String msg, Throwable e) { + logger.error(msg, e); + return new IllegalStateException(msg, e); + } + + public IllegalArgumentException getIllegalArgumentException(String msg) { + return getIllegalArgumentException(msg, null); + } + + public IllegalArgumentException getIllegalArgumentException(String msg, Throwable e) { + logger.error(msg, e); + return new IllegalArgumentException(msg, e); + } + + public UnsupportedOperationException getUnsupportedOperationException(String msg) { + logger.error(msg); + return new UnsupportedOperationException(msg); + } + + public NotFoundException getNotFoundException(String reason) { + logger.error(reason); + return new NotFoundException(reason); + } + + public ErrorInfo getErrorInfo(CharSequence requestUrl, Exception e) { + logger.error("Exception at request " + requestUrl); + return new ErrorInfo(requestUrl, e); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/Main.java b/src/main/java/ru/javawebinar/topjava/Main.java index b23a2f0961fc..9938c1371cb5 100644 --- a/src/main/java/ru/javawebinar/topjava/Main.java +++ b/src/main/java/ru/javawebinar/topjava/Main.java @@ -1,14 +1,32 @@ package ru.javawebinar.topjava; +import java.util.function.Consumer; + /** * User: gkislin * Date: 05.08.2015 * - * @link http://caloriesmng.herokuapp.com/ + * @link http://javawebinar.ru/topjava/ * @link https://github.com/JavaOPs/topjava */ public class Main { public static void main(String[] args) { + execute(() -> { + System.out.println("Hello Topjava!"); + }); + consume(System.out::println, "Hello Topjava!"); + } + + private static void execute(Runnable runnable) { + System.out.println("Start runner"); + runnable.run(); + System.out.println("End runner"); + } + + private static void consume(Consumer consumer, String out) { + System.out.println("Start consume"); + consumer.accept(out); + System.out.println("End consume"); System.out.format("Hello Topjava Enterprise!"); } } diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java new file mode 100644 index 000000000000..a3f5266a97ec --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -0,0 +1,14 @@ +package ru.javawebinar.topjava; + +/** + * User: gkislin + * Date: 19.08.2014 + */ +public class Profiles { + public static final String + POSTGRES = "postgres", + HSQLDB = "hsqldb", + JDBC = "jdbc", + JPA = "jpa", + DATAJPA = "datajpa"; +} diff --git a/src/main/java/ru/javawebinar/topjava/model/BaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/BaseEntity.java new file mode 100644 index 000000000000..c7d1bb2b0dae --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/BaseEntity.java @@ -0,0 +1,63 @@ +package ru.javawebinar.topjava.model; + +import ru.javawebinar.topjava.LoggerWrapper; + +import javax.persistence.*; + +/** + * User: gkislin + * Date: 22.08.2014 + */ +@MappedSuperclass +@Access(AccessType.FIELD) +//@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE) +public class BaseEntity { + protected static final LoggerWrapper LOG = LoggerWrapper.get(BaseEntity.class); + + public static final int START_SEQ = 100000; + + @Id + @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") + protected Integer id; + + public BaseEntity() { + } + + protected BaseEntity(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 boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BaseEntity that = (BaseEntity) o; + if (id == null || that.id == null) { + throw LOG.getIllegalStateException("Equals '" + this + "' and '" + that + "' with null id"); + } + return id.equals(that.id); + } + + @Override + public int hashCode() { + return (id == null) ? 0 : id; + } + +} diff --git a/src/main/java/ru/javawebinar/topjava/model/NamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/NamedEntity.java new file mode 100644 index 000000000000..e593d37981e7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/NamedEntity.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.validator.constraints.NotEmpty; + +import javax.persistence.Column; +import javax.persistence.MappedSuperclass; + +/** + * User: gkislin + * Date: 22.08.2014 + */ +@MappedSuperclass +public class NamedEntity extends BaseEntity { + + @NotEmpty + @Column(name = "name", nullable = false) + protected String name; + + public NamedEntity() { + } + + protected NamedEntity(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 name; + } +} 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..9c5210417f0b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -0,0 +1,17 @@ +package ru.javawebinar.topjava.model; + +import org.springframework.security.core.GrantedAuthority; + +/** + * User: gkislin + * Date: 22.08.2014 + */ +public enum Role implements GrantedAuthority { + ROLE_USER, + ROLE_ADMIN; + + @Override + public String getAuthority() { + return name(); + } +} 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..78dad62369fc --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -0,0 +1,149 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.persistence.*; +import javax.validation.constraints.Digits; +import java.util.*; + +import static ru.javawebinar.topjava.util.UserUtil.DEFAULT_CALORIES_PER_DAY; + +/** + * User: gkislin + * Date: 22.08.2014 + */ +@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 DISTINCT(u) FROM User u LEFT JOIN FETCH u.roles ORDER BY u.name, u.email"), +}) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@Entity +@Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "email", name = "unique_email")}) +public class User extends NamedEntity { + + public static final String DELETE = "User.delete"; + public static final String ALL_SORTED = "User.getAllSorted"; + public static final String BY_EMAIL = "User.getByEmail"; + + @Column(name = "email", nullable = false, unique = true) + @Email + @NotEmpty + protected String email; + + @Column(name = "password", nullable = false) + @NotEmpty + @Length(min = 5) + protected String password; + + @Column(name = "enabled", nullable = false) + protected boolean enabled = true; + + @Column(name = "registered", columnDefinition = "timestamp default now()") + protected Date registered = new Date(); + + @Enumerated(EnumType.STRING) + @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id")) + @Column(name = "role") + @ElementCollection(fetch = FetchType.EAGER) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + protected Set roles; + + @Column(name = "calories_per_day", columnDefinition = "default 2000") + @Digits(fraction = 0, integer = 4) + protected int caloriesPerDay = DEFAULT_CALORIES_PER_DAY; + + @OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, mappedBy = "user") + @OrderBy("dateTime DESC") +// @JsonIgnore + protected List meals; + + public User() { + } + + public User(User u) { + this(u.getId(), u.getName(), u.getEmail(), u.getPassword(), u.getCaloriesPerDay(), u.isEnabled(), u.getRoles()); + } + + public User(Integer id, String name, String email, String password, int caloriesPerDay, Role role, Role... roles) { + this(id, name, email, password, caloriesPerDay, true, 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; + } + + public List getMeals() { + return meals; + } + + + public void setRoles(Collection roles) { + this.roles = EnumSet.copyOf(roles); + } + + @Override + public String toString() { + return "User (" + + "id=" + id + + ", email=" + email + + ", name=" + name + + ", enabled=" + enabled + + ", roles=" + roles + + ", caloriesPerDay=" + caloriesPerDay + + ')'; + } +} 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..6c8f3e14d8b7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/model/UserMeal.java @@ -0,0 +1,107 @@ +package ru.javawebinar.topjava.model; + +import org.hibernate.validator.constraints.NotEmpty; +import org.hibernate.validator.constraints.Range; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * GKislin + * 11.01.2015. + */ +@NamedQueries({ + @NamedQuery(name = UserMeal.GET, query = "SELECT m FROM UserMeal m WHERE m.id=:id AND m.user.id=:userId"), + @NamedQuery(name = UserMeal.ALL_SORTED, query = "SELECT m FROM UserMeal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC"), + @NamedQuery(name = UserMeal.DELETE, query = "DELETE FROM UserMeal m WHERE m.id=:id AND m.user.id=:userId"), + @NamedQuery(name = UserMeal.GET_BETWEEN, + query = "SELECT m from UserMeal m WHERE m.user.id=:userId " + + " AND m.dateTime BETWEEN :startDate AND :endDate ORDER BY m.dateTime DESC"), + +// @NamedQuery(name = UserMeal.UPDATE, query = "UPDATE UserMeal m SET m.dateTime = :datetime, m.calories= :calories," + +// "m.description=:desc where m.id=:id and m.user.id=:userId") +}) +@Entity +@Table(name = "meals") +public class UserMeal extends BaseEntity { + public static final String GET = "UserMeal.get"; + public static final String ALL_SORTED = "UserMeal.getAll"; + public static final String DELETE = "UserMeal.delete"; + public static final String GET_BETWEEN = "UserMeal.getBetween"; + + @Column(name = "date_time", nullable = false) + @NotNull + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + protected LocalDateTime dateTime; + + @Column(name = "description", nullable = false) + @NotEmpty + protected String description; + + @Column(name = "calories") + @Range(min = 10, max = 5000) + @NotNull + protected Integer calories; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + public UserMeal() { + } + + public UserMeal(LocalDateTime dateTime, String description, int calories) { + this(null, dateTime, description, calories); + } + + public UserMeal(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 void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setCalories(Integer calories) { + this.calories = calories; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + @Override + public String toString() { + return "UserMeal{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java new file mode 100644 index 000000000000..75b3d82ba3cb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/JpaUtil.java @@ -0,0 +1,25 @@ +package ru.javawebinar.topjava.repository; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +public class JpaUtil { + + @PersistenceContext + private EntityManager em; + + public void clear2ndLevelHibernateCache() { + Session s = (Session) em.getDelegate(); + SessionFactory sf = s.getSessionFactory(); +// sf.evict(User.class); +// sf.getCache().evictEntity(User.class, BaseEntity.START_SEQ); +// sf.getCache().evictEntityRegion(User.class); + sf.getCache().evictQueryRegions(); + sf.getCache().evictDefaultQueryRegion(); + sf.getCache().evictCollectionRegions(); + sf.getCache().evictEntityRegions(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserMealRepository.java new file mode 100644 index 000000000000..afe485740dbe --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/UserMealRepository.java @@ -0,0 +1,31 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.UserMeal; + +import java.time.LocalDateTime; +import java.util.Collection; + +/** + * GKislin + * 06.03.2015. + */ +public interface UserMealRepository { + // null if updated meal do not belong to userId + UserMeal save(UserMeal userMeal, int userId); + + // false if meal do not belong to userId + boolean delete(int id, int userId); + + // null if meal do not belong to userId + UserMeal get(int id, int userId); + + // ORDERED DATE, TIME + Collection getAll(int userId); + + // ORDERED DATE, TIME + Collection getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId); + + default UserMeal getWithUser(Integer id, Integer userId) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java new file mode 100644 index 000000000000..cd331a2cea07 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/UserRepository.java @@ -0,0 +1,28 @@ +package ru.javawebinar.topjava.repository; + +import ru.javawebinar.topjava.model.User; + +import java.util.Collection; + +/** + * User: gkislin + * Date: 22.08.2014 + */ +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); + + Collection getAll(); + + default User getWithMeals(int id){ + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserMealRepositoryImpl.java new file mode 100644 index 000000000000..5a633070298d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserMealRepositoryImpl.java @@ -0,0 +1,58 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.repository.UserMealRepository; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * GKislin + * 27.03.2015. + */ +@Repository +public class DataJpaUserMealRepositoryImpl implements UserMealRepository { + @Autowired + private ProxyUserMealRepository proxy; + + @Autowired + private ProxyUserRepository userProxy; + + @Override + @Transactional + public UserMeal save(UserMeal userMeal, int userId) { + if (!userMeal.isNew() && get(userMeal.getId(), userId) == null) { + return null; + } + userMeal.setUser(userProxy.getOne(userId)); + return proxy.save(userMeal); + } + + @Override + public boolean delete(int id, int userId) { + return proxy.delete(id, userId) != 0; + } + + @Override + public UserMeal get(int id, int userId) { + return proxy.get(id, userId); + } + + @Override + public List getAll(int userId) { + return proxy.getAll(userId); + } + + @Override + public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + return proxy.getBetween(startDate, endDate, userId); + } + + @Override + public UserMeal getWithUser(Integer id, Integer userId) { + return proxy.getWithUser(id, userId); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java new file mode 100644 index 000000000000..422478684ec4 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/DataJpaUserRepositoryImpl.java @@ -0,0 +1,52 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import java.util.List; + +/** + * GKislin + * 27.03.2015. + */ + +@Repository +public class DataJpaUserRepositoryImpl implements UserRepository { + private static final Sort SORT_NAME_EMAIL = new Sort("name", "email"); + + @Autowired + private ProxyUserRepository proxy; + + @Override + public User save(User user) { + return proxy.save(user); + } + + @Override + public boolean delete(int id) { + return proxy.delete(id) != 0; + } + + @Override + public User get(int id) { + return proxy.findOne(id); + } + + @Override + public User getByEmail(String email) { + return proxy.getByEmail(email); + } + + @Override + public List getAll() { + return proxy.findAll(SORT_NAME_EMAIL); + } + + @Override + public User getWithMeals(int id) { + return proxy.getWithMeals(id); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/ProxyUserMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/ProxyUserMealRepository.java new file mode 100644 index 000000000000..2e13b933050c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/ProxyUserMealRepository.java @@ -0,0 +1,40 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.UserMeal; + +import java.time.LocalDateTime; +import java.util.List; + +@Transactional(readOnly = true) +public interface ProxyUserMealRepository extends JpaRepository { + + @Modifying + @Transactional + @Query("DELETE FROM UserMeal m WHERE m.id=:id AND m.user.id=:userId") + int delete(@Param("id") int id, @Param("userId") int userId); + + @Override + UserMeal save(UserMeal item); + + @Query("SELECT m FROM UserMeal m WHERE m.id=:id AND m.user.id=:userId") + UserMeal get(@Param("id") int id, @Param("userId") int userId); + + @Query("SELECT m FROM UserMeal m WHERE m.user.id=:userId ORDER BY m.dateTime DESC") + List getAll(@Param("userId") int userId); + + @Modifying + @Transactional + @Query("DELETE FROM UserMeal m WHERE m.user.id=:userId") + void deleteAll(@Param("userId") int userId); + + @Query("SELECT m from UserMeal m WHERE m.user.id=:userId AND m.dateTime BETWEEN :startDate AND :endDate ORDER BY m.dateTime DESC") + List getBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("userId") int userId); + + @Query("SELECT um FROM UserMeal um JOIN FETCH um.user WHERE um.id = ?1 and um.user.id = ?2 ORDER BY um.dateTime DESC") + UserMeal getWithUser(Integer id, Integer userId); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/datajpa/ProxyUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/datajpa/ProxyUserRepository.java new file mode 100644 index 000000000000..618f05eceaa7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/datajpa/ProxyUserRepository.java @@ -0,0 +1,40 @@ +package ru.javawebinar.topjava.repository.datajpa; + +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; + +import java.util.List; + +/** + * GKislin + * 27.03.2015. + */ +@Transactional(readOnly = true) +public interface ProxyUserRepository extends JpaRepository { + + @Transactional + @Modifying +// @Query(name = User.DELETE) + @Query("DELETE FROM User u WHERE u.id=:id") + int delete(@Param("id") int id); + + @Override + @Transactional + User save(User user); + + @Override + User findOne(Integer id); + + @Override + List findAll(Sort sort); + + User getByEmail(String email); + + @Query("SELECT u FROM User u JOIN FETCH u.meals WHERE u.id = ?1") + User getWithMeals(Integer id); +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserMealRepositoryImpl.java new file mode 100644 index 000000000000..31e802a837a3 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserMealRepositoryImpl.java @@ -0,0 +1,96 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.repository.UserMealRepository; + +import javax.sql.DataSource; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; + +/** + * User: gkislin + * Date: 26.08.2014 + */ + +@Repository +@Transactional(readOnly = true) +public class JdbcUserMealRepositoryImpl implements UserMealRepository { + + private static final RowMapper ROW_MAPPER = + (rs, rowNum) -> + new UserMeal(rs.getInt("id"), rs.getTimestamp("date_time").toLocalDateTime(), + rs.getString("description"), rs.getInt("calories")); + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private SimpleJdbcInsert insertUserMeal; + + @Autowired + public JdbcUserMealRepositoryImpl(DataSource dataSource) { + this.insertUserMeal = new SimpleJdbcInsert(dataSource) + .withTableName("meals") + .usingGeneratedKeyColumns("id"); + } + + @Override + @Transactional + public UserMeal save(UserMeal userMeal, int userId) { + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", userMeal.getId()) + .addValue("description", userMeal.getDescription()) + .addValue("calories", userMeal.getCalories()) + .addValue("date_time", Timestamp.valueOf(userMeal.getDateTime())) + .addValue("user_id", userId); + + if (userMeal.isNew()) { + Number newId = insertUserMeal.executeAndReturnKey(map); + userMeal.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 userMeal; + } + + @Override + @Transactional + public boolean delete(int id, int userId) { + return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0; + } + + @Override + public UserMeal get(int id, int userId) { + List userMeals = jdbcTemplate.query( + "SELECT * FROM meals WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId); + return DataAccessUtils.singleResult(userMeals); + } + + 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, Timestamp.valueOf(startDate), Timestamp.valueOf(endDate)); + } +} \ No newline at end of file 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..e566c833f167 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepositoryImpl.java @@ -0,0 +1,156 @@ +package ru.javawebinar.topjava.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * User: gkislin + * Date: 26.08.2014 + */ + +@Repository +@Transactional(readOnly = true) +public class JdbcUserRepositoryImpl implements UserRepository { + + private static final BeanPropertyRowMapper ROW_MAPPER = BeanPropertyRowMapper.newInstance(User.class); + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private SimpleJdbcInsert insertUser; + + @Autowired + public JdbcUserRepositoryImpl(DataSource dataSource) { + this.insertUser = new SimpleJdbcInsert(dataSource) + .withTableName("USERS") + .usingGeneratedKeyColumns("id"); + } + + @Override + @Transactional + public User save(User user) { + MapSqlParameterSource map = new MapSqlParameterSource() + .addValue("id", user.getId()) + .addValue("name", user.getName()) + .addValue("email", user.getEmail()) + .addValue("password", user.getPassword()) + .addValue("registered", user.getRegistered()) + .addValue("enabled", user.isEnabled()) + .addValue("caloriesPerDay", user.getCaloriesPerDay()); + + if (user.isNew()) { + Number newKey = insertUser.executeAndReturnKey(map); + user.setId(newKey.intValue()); + insertRoles(user); + } else { + deleteRoles(user); + insertRoles(user); + namedParameterJdbcTemplate.update( + "UPDATE users SET name=:name, email=:email, password=:password, " + + "registered=:registered, enabled=:enabled, calories_per_day=:caloriesPerDay WHERE id=:id", map); + } + return user; + } + + @Override + @Transactional + public boolean delete(int id) { + return jdbcTemplate.update("DELETE FROM users WHERE id=?", id) != 0; + } + + @Override + public User get(int id) { + List users = jdbcTemplate.query("SELECT * FROM users WHERE id=?", ROW_MAPPER, id); + return setRoles(DataAccessUtils.singleResult(users)); + } + + @Override + public User getByEmail(String email) { + return setRoles(jdbcTemplate.queryForObject("SELECT * FROM users WHERE email=?", ROW_MAPPER, email)); + } + + @Override + public List getAll() { + List users = jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); + + class UserRole { + public UserRole(Role role, int userId) { + this.role = role; + this.userId = userId; + } + + final private Role role; + final private int userId; + + public Role getRole() { + return role; + } + + public int getUserId() { + return userId; + } + } + + Map> userRoles = jdbcTemplate.query("SELECT role, user_id FROM user_roles", + (rs, rowNum) -> new UserRole(Role.valueOf(rs.getString("role")), rs.getInt("user_id"))) + .stream().collect(Collectors.groupingBy(UserRole::getUserId)); + + users.forEach(u -> u.setRoles(userRoles.get(u.getId()).stream().map(UserRole::getRole).collect(Collectors.toList()))); + return users; + } + + private void insertRoles(User u) { + Set roles = u.getRoles(); + Iterator iterator = roles.iterator(); + + jdbcTemplate.batchUpdate("INSERT INTO user_roles (user_id, role) VALUES (?, ?)", + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setInt(1, u.getId()); + ps.setString(2, iterator.next().name()); + } + + @Override + public int getBatchSize() { + return roles.size(); + } + }); + } + + private void deleteRoles(User u) { + jdbcTemplate.update("DELETE FROM user_roles WHERE user_id=?", u.getId()); + } + + private User setRoles(User u) { + if (u != null) { + List roles = jdbcTemplate.query("SELECT role FROM user_roles WHERE user_id=?", + (rs, rowNum) -> Role.valueOf(rs.getString("role")), u.getId()); + u.setRoles(roles); + } + return u; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserMealRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserMealRepositoryImpl.java new file mode 100644 index 000000000000..b8411e48843b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserMealRepositoryImpl.java @@ -0,0 +1,74 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.dao.support.DataAccessUtils; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.repository.UserMealRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.time.LocalDateTime; +import java.util.List; + +/** + * User: gkisline + * Date: 26.08.2014 + */ + +@Repository +@Transactional(readOnly = true) +public class JpaUserMealRepositoryImpl implements UserMealRepository { + + @PersistenceContext + private EntityManager em; + + @Override + @Transactional + public UserMeal save(UserMeal userMeal, int userId) { + User ref = em.getReference(User.class, userId); + userMeal.setUser(ref); + + if (userMeal.isNew()) { + em.persist(userMeal); + return userMeal; + } else { + if (get(userMeal.getId(), userId) == null) return null; + return em.merge(userMeal); + } + } + + @Override + @Transactional + public boolean delete(int id, int userId) { + return em.createNamedQuery(UserMeal.DELETE) + .setParameter("id", id) + .setParameter("userId", userId) + .executeUpdate() != 0; + } + + @Override + public UserMeal get(int id, int userId) { + List userMeals = em.createNamedQuery(UserMeal.GET, UserMeal.class) + .setParameter("id", id) + .setParameter("userId", userId) + .getResultList(); + return DataAccessUtils.singleResult(userMeals); + } + + @Override + public List getAll(int userId) { + return em.createNamedQuery(UserMeal.ALL_SORTED, UserMeal.class) + .setParameter("userId", userId) + .getResultList(); + } + + @Override + public List getBetween(LocalDateTime startDate, LocalDateTime endDate, int userId) { + return em.createNamedQuery(UserMeal.GET_BETWEEN, UserMeal.class) + .setParameter("userId", userId) + .setParameter("startDate", startDate) + .setParameter("endDate", endDate).getResultList(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java new file mode 100644 index 000000000000..2b4057f3a4b4 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaUserRepositoryImpl.java @@ -0,0 +1,70 @@ +package ru.javawebinar.topjava.repository.jpa; + +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import java.util.List; + +/** + * User: gkislin + * Date: 29.08.2014 + */ +@Repository +@Transactional(readOnly = true) +public class JpaUserRepositoryImpl implements UserRepository { + +/* + @Autowired + private SessionFactory sessionFactory; + + private Session openSession() { + return sessionFactory.getCurrentSession(); + } +*/ + + @PersistenceContext + private EntityManager em; + + @Override + @Transactional + public User save(User user) { + if (user.isNew()) { + em.persist(user); + return user; + } else { + return em.merge(user); + } + } + + @Override + public User get(int id) { + return em.find(User.class, id); + } + + @Override + @Transactional + public boolean delete(int id) { + +/* User ref = em.getReference(User.class, id); + em.remove(ref); + + Query query = em.createQuery("DELETE FROM User u WHERE u.id=:id"); + return query.setParameter("id", id).executeUpdate() != 0; +*/ + return em.createNamedQuery(User.DELETE).setParameter("id", id).executeUpdate() != 0; + } + + @Override + public User getByEmail(String email) { + return em.createNamedQuery(User.BY_EMAIL, User.class).setParameter(1, email).getSingleResult(); + } + + @Override + public List getAll() { + return em.createNamedQuery(User.ALL_SORTED, User.class).getResultList(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/service/UserMealService.java b/src/main/java/ru/javawebinar/topjava/service/UserMealService.java new file mode 100644 index 000000000000..2af9320798c3 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserMealService.java @@ -0,0 +1,32 @@ +package ru.javawebinar.topjava.service; + +import ru.javawebinar.topjava.model.UserMeal; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collection; + +/** + * GKislin + * 15.06.2015. + */ +public interface UserMealService { + UserMeal get(int id, int userId); + + void delete(int id, int userId); + + default Collection getBetweenDates(LocalDate startDate, LocalDate endDate, int userId) { + return getBetweenDateTimes(LocalDateTime.of(startDate, LocalTime.MIN), LocalDateTime.of(endDate, LocalTime.MAX), userId); + } + + Collection getBetweenDateTimes(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId); + + Collection getAll(int userId); + + UserMeal update(UserMeal meal, int userId); + + UserMeal save(UserMeal meal, int userId); + + UserMeal getWithUser(Integer id, Integer userId); +} diff --git a/src/main/java/ru/javawebinar/topjava/service/UserMealServiceImpl.java b/src/main/java/ru/javawebinar/topjava/service/UserMealServiceImpl.java new file mode 100644 index 000000000000..4f05d0b7eb5a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserMealServiceImpl.java @@ -0,0 +1,63 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.repository.UserMealRepository; +import ru.javawebinar.topjava.util.exception.ExceptionUtil; + +import java.time.LocalDateTime; +import java.util.Collection; + +/** + * GKislin + * 06.03.2015. + */ +@Service +public class UserMealServiceImpl implements UserMealService { + + @Autowired + private UserMealRepository repository; + + @Override + public UserMeal get(int id, int userId) { + return ExceptionUtil.check(repository.get(id, userId), id); + } + + @Override + public void delete(int id, int userId) { + ExceptionUtil.check(repository.delete(id, userId), id); + } + + @Override + public Collection getBetweenDateTimes(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return repository.getBetween(startDateTime, endDateTime, userId); + } + + @Override + public Collection getAll(int userId) { + return repository.getAll(userId); + } + + @Override + public UserMeal update(UserMeal meal, int userId) { + return ExceptionUtil.check(repository.save(meal, userId), meal.getId()); + } + + @Override + public UserMeal save(UserMeal meal, int userId) { + return repository.save(meal, userId); + } + +// @Autowired +// private UserRepository userRepository; + + @Transactional + public UserMeal getWithUser(Integer id, Integer userId) { +// UserMeal meal = get(id, userId); +// meal.setUser(userRepository.get(userId)); +// return meal; + return repository.getWithUser(id, userId); + } +} 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..8f669330a5e7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -0,0 +1,35 @@ +package ru.javawebinar.topjava.service; + + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Collection; + +/** + * User: gkislin + * Date: 22.08.2014 + */ +public interface UserService { + + User save(User user); + + void delete(int id) throws NotFoundException; + + User get(int id) throws NotFoundException; + + User getByEmail(String email) throws NotFoundException; + + Collection getAll(); + + void update(UserTo user); + + void update(User user); + + void evictCache(); + + void enable(int id, boolean enable); + + User getWithMeals(int id); +} 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..ac013a22fc57 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/service/UserServiceImpl.java @@ -0,0 +1,101 @@ +package ru.javawebinar.topjava.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.javawebinar.topjava.LoggedUser; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; +import ru.javawebinar.topjava.util.exception.ExceptionUtil; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Collection; +import java.util.Objects; + +/** + * GKislin + * 06.03.2015. + */ +@Service("userService") +public class UserServiceImpl implements UserService, UserDetailsService { + + @Autowired + private UserRepository repository; + + @CacheEvict(value = "users", allEntries = true) + @Override + public User save(User user) { + return repository.save(user); + } + + @CacheEvict(value = "users", allEntries = true) + @Override + public void delete(int id) { + ExceptionUtil.check(repository.delete(id), id); + } + + @Override + public User get(int id) throws NotFoundException { + return ExceptionUtil.check(repository.get(id), id); + } + + @Override + public User getByEmail(String email) throws NotFoundException { + Objects.requireNonNull(email, "Email must not be empty"); + return ExceptionUtil.check(repository.getByEmail(email), "email=" + email); + } + + @Cacheable("users") + @Override + public Collection getAll() { + return repository.getAll(); + } + + @CacheEvict(value = "users", allEntries = true) + public void update(User user) { + repository.save(user); + } + + @CacheEvict(value = "users", allEntries = true) + @Transactional + @Override + public void update(UserTo userTo) { + User user = get(userTo.getId()); + repository.save(UserUtil.updateFromTo(user, userTo)); + } + + + @CacheEvict(value = "users", allEntries = true) + @Override + public void evictCache() { + } + + @CacheEvict(value = "users", allEntries = true) + @Override + @Transactional + public void enable(int id, boolean enabled) { + User user = get(id); + user.setEnabled(enabled); + repository.save(user); + } + + @Override + public LoggedUser loadUserByUsername(String email) throws UsernameNotFoundException { + User u = repository.getByEmail(email.toLowerCase()); + if (u == null) { + throw new UsernameNotFoundException("User " + email + " is not found"); + } + return new LoggedUser(u); + } + + @Override + public User getWithMeals(int id) { + return repository.getWithMeals(id); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/to/UserMealWithExceed.java b/src/main/java/ru/javawebinar/topjava/to/UserMealWithExceed.java new file mode 100644 index 000000000000..3441774f0854 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/UserMealWithExceed.java @@ -0,0 +1,68 @@ +package ru.javawebinar.topjava.to; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.LocalDateTime; + +/** + * GKislin + * 11.01.2015. + */ +public class UserMealWithExceed { + protected final Integer id; + + protected final LocalDateTime dateTime; + + protected final String description; + + protected final int calories; + + protected final boolean exceed; + + public UserMealWithExceed(LocalDateTime dateTime, String description, int calories, boolean exceed) { + this(null, dateTime, description, calories, exceed); + } + + public UserMealWithExceed(@JsonProperty("id") Integer id, + @JsonProperty("dateTime") LocalDateTime dateTime, + @JsonProperty("description") String description, + @JsonProperty("calories") int calories, + @JsonProperty("exceed") 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; + } + + public String getDescription() { + return description; + } + + public int getCalories() { + return calories; + } + + public boolean isExceed() { + return exceed; + } + + @Override + public String toString() { + return "UserMealWithExceed{" + + "id=" + id + + ", dateTime=" + dateTime + + ", description='" + description + '\'' + + ", calories=" + calories + + ", exceed=" + exceed + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java new file mode 100644 index 000000000000..263dc7526113 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -0,0 +1,96 @@ +package ru.javawebinar.topjava.to; + +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.NotEmpty; +import org.hibernate.validator.constraints.Range; +import ru.javawebinar.topjava.util.UserUtil; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; + +public class UserTo implements Serializable { + static final long serialVersionUID = 1L; + + protected int id; + + @NotEmpty + protected String name; + + @Email + @NotEmpty + protected String email; + + @Size(min = 5, max = 64, message = " must between 5 and 64 characters") + protected String password; + + + @Range(min = 500, max = 5000) + @NotNull(message = " must not be empty") + protected Integer caloriesPerDay = UserUtil.DEFAULT_CALORIES_PER_DAY; + + public UserTo() { + } + + public UserTo(int id, String name, String email, String password, int caloriesPerDay) { + this.id = id; + this.name = name; + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + } + + public void setId(int id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = (id == null ? 0 : id); + } + + public void setName(String name) { + this.name = name; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public Integer getCaloriesPerDay() { + return caloriesPerDay; + } + + public void setCaloriesPerDay(Integer caloriesPerDay) { + this.caloriesPerDay = 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/PasswordUtil.java b/src/main/java/ru/javawebinar/topjava/util/PasswordUtil.java new file mode 100644 index 000000000000..eee4b3f86167 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/PasswordUtil.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.StringUtils; + +import java.util.regex.Pattern; + +/** + * GKislin + * 05.01.2015. + */ + +public class PasswordUtil { + private static final Pattern BCRYPT_PATTERN = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}"); + static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder(); + + public static PasswordEncoder getPasswordEncoder() { + return PASSWORD_ENCODER; + } + + public static String encode(String newPassword) { + if (StringUtils.isEmpty(newPassword)) { + return null; + } + if (isEncoded(newPassword)) { + return newPassword; + } + return PASSWORD_ENCODER.encode(newPassword); + } + + public static boolean isMatch(String rawPassword, String password) { + return PASSWORD_ENCODER.matches(rawPassword, password); + } + + public static boolean isEncoded(String newPassword) { + return BCRYPT_PATTERN.matcher(newPassword).matches(); + } +} 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..abc977567e19 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/TimeUtil.java @@ -0,0 +1,46 @@ +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; + +/** + * GKislin + * 07.01.2015. + */ +public class TimeUtil { + public static final DateTimeFormatter DATE_TME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + public static final LocalDate MIN_DATE = LocalDate.of(0, 1, 1); + public static final LocalDate MAX_DATE = LocalDate.of(3000, 1, 1); + + public static boolean isBetween(LocalTime lt, LocalTime startTime, LocalTime endTime) { + return lt.compareTo(startTime) >= 0 && lt.compareTo(endTime) <= 0; + } + + public static boolean isBetween(LocalDateTime ldt, LocalDateTime startDateTime, LocalDateTime endDateTime) { + return ldt.compareTo(startDateTime) >= 0 && ldt.compareTo(endDateTime) <= 0; + } + + public static String toString(LocalDateTime ldt) { + return ldt == null ? "" : ldt.format(DATE_TME_FORMATTER); + } + + public static LocalDate parseLocalDate(String str, LocalDate def) { + return StringUtils.isEmpty(str) ? def : LocalDate.parse(str); + } + + public static LocalTime parseLocalTime(String str, LocalTime def) { + return StringUtils.isEmpty(str) ? def : LocalTime.parse(str); + } + + public static LocalDateTime parseLocalDateTime(String str) { + return parseLocalDateTime(str, DATE_TME_FORMATTER); + } + + public static LocalDateTime parseLocalDateTime(String str, DateTimeFormatter formatter) { + return StringUtils.isEmpty(str) ? LocalDateTime.now() : LocalDateTime.parse(str, formatter); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java new file mode 100644 index 000000000000..3d8ab8224f94 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/UserMealsUtil.java @@ -0,0 +1,69 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.to.UserMealWithExceed; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.util.*; +import java.util.stream.Collectors; + +/** + * GKislin + * 31.05.2015. + */ +public class UserMealsUtil { + public static final List MEAL_LIST = 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) + ); + + public static void main(String[] args) { + List filteredMealsWithExceeded = getFilteredWithExceeded(MEAL_LIST, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000); + filteredMealsWithExceeded.forEach(System.out::println); + + System.out.println(getFilteredWithExceededByCycle(MEAL_LIST, LocalTime.of(7, 0), LocalTime.of(12, 0), 2000)); + } + + public static List getWithExceeded(Collection mealList, int caloriesPerDay) { + return getFilteredWithExceeded(mealList, LocalTime.MIN, LocalTime.MAX, caloriesPerDay); + } + + public static List getFilteredWithExceeded(Collection mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + Map caloriesSumByDate = mealList.stream().collect(Collectors.groupingBy(um -> um.getDateTime().toLocalDate(), + Collectors.summingInt(UserMeal::getCalories))); + + return mealList.stream() + .filter(um -> TimeUtil.isBetween(um.getDateTime().toLocalTime(), startTime, endTime)) + .map(um -> createWithExceed(um, caloriesSumByDate.get(um.getDateTime().toLocalDate()) > caloriesPerDay)) + .collect(Collectors.toList()); + } + + public static UserMealWithExceed createWithExceed(UserMeal um, boolean exceeded) { + return new UserMealWithExceed(um.getId(), um.getDateTime(), um.getDescription(), um.getCalories(), exceeded); + } + + public static List getFilteredWithExceededByCycle(List mealList, LocalTime startTime, LocalTime endTime, int caloriesPerDay) { + + Map caloriesSumPerDate = new HashMap<>(); + for (UserMeal meal : mealList) { + caloriesSumPerDate.merge(meal.getDateTime().toLocalDate(), meal.getCalories(), Integer::sum); + } + + List mealExceeded = new ArrayList<>(); + for (UserMeal meal : mealList) { + LocalDateTime dateTime = meal.getDateTime(); + if (TimeUtil.isBetween(dateTime.toLocalTime(), startTime, endTime)) { + mealExceeded.add(createWithExceed(meal, caloriesSumPerDate.get(dateTime.toLocalDate()) > caloriesPerDay)); + } + } + return mealExceeded; + } + +} diff --git a/src/main/java/ru/javawebinar/topjava/util/UserUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java new file mode 100644 index 000000000000..db3310206e15 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java @@ -0,0 +1,34 @@ +package ru.javawebinar.topjava.util; + +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; + +/** + * GKislin + */ +public class UserUtil { + public static final int DEFAULT_CALORIES_PER_DAY = 2000; + + public static User createFromTo(UserTo newUser) { + return new User(null, newUser.getName(), newUser.getEmail().toLowerCase(), newUser.getPassword(), newUser.getCaloriesPerDay(), 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()); + user.setPassword(userTo.getPassword()); + user.setCaloriesPerDay(userTo.getCaloriesPerDay()); + return prepareToSave(user); + } + + public static User prepareToSave(User user) { + user.setPassword(PasswordUtil.encode(user.getPassword())); + user.setEmail(user.getEmail().toLowerCase()); + return user; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/converter/LocalDateFormatter.java b/src/main/java/ru/javawebinar/topjava/util/converter/LocalDateFormatter.java new file mode 100644 index 000000000000..193b32de9406 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/converter/LocalDateFormatter.java @@ -0,0 +1,26 @@ +package ru.javawebinar.topjava.util.converter; + +import org.springframework.format.Formatter; +import ru.javawebinar.topjava.util.TimeUtil; + +import java.text.ParseException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** + * GKislin + * 15.04.2015. + */ +public class LocalDateFormatter implements Formatter { + + @Override + public LocalDate parse(String text, Locale locale) throws ParseException { + return TimeUtil.parseLocalDate(text, null); + } + + @Override + public String print(LocalDate lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_DATE); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/converter/LocalTimeFormatter.java b/src/main/java/ru/javawebinar/topjava/util/converter/LocalTimeFormatter.java new file mode 100644 index 000000000000..1a29f9c5accd --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/converter/LocalTimeFormatter.java @@ -0,0 +1,26 @@ +package ru.javawebinar.topjava.util.converter; + +import org.springframework.format.Formatter; +import ru.javawebinar.topjava.util.TimeUtil; + +import java.text.ParseException; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +/** + * GKislin + * 15.04.2015. + */ +public class LocalTimeFormatter implements Formatter { + + @Override + public LocalTime parse(String text, Locale locale) throws ParseException { + return TimeUtil.parseLocalTime(text, null); + } + + @Override + public String print(LocalTime lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_TIME); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java new file mode 100644 index 000000000000..c422c409e630 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -0,0 +1,17 @@ +package ru.javawebinar.topjava.util.exception; + +/** + * User: gkislin + * Date: 19.08.2014 + */ +public class ErrorInfo { + public final String url; + public final String cause; + public final String detail; + + public ErrorInfo(CharSequence url, Throwable ex) { + this.url = url.toString(); + this.cause = ex.getClass().getSimpleName(); + this.detail = ex.getLocalizedMessage(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ExceptionUtil.java b/src/main/java/ru/javawebinar/topjava/util/exception/ExceptionUtil.java new file mode 100644 index 000000000000..295aadab347f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ExceptionUtil.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.util.exception; + + +import ru.javawebinar.topjava.LoggerWrapper; + +/** + * User: gkislin + * Date: 14.05.2014 + */ +public class ExceptionUtil { + private static final LoggerWrapper LOG = LoggerWrapper.get(ExceptionUtil.class); + + public static void check(boolean found, int id) { + check(found, "id=" + id); + } + + public static T check(T object, int id) { + return check(object, "id=" + id); + } + + public static T check(T object, String msg) { + check(object != null, msg); + return object; + } + + public static void check(boolean found, String msg) { + if (!found) throw LOG.getNotFoundException("Not found entity with " + msg); + } +} 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..1c85842a3497 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/NotFoundException.java @@ -0,0 +1,15 @@ +package ru.javawebinar.topjava.util.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * User: gkislin + * Date: 19.08.2014 + */ +@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No data found") // 404 +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ValidationException.java b/src/main/java/ru/javawebinar/topjava/util/exception/ValidationException.java new file mode 100644 index 000000000000..4a912734cbf8 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ValidationException.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.util.exception; + +import org.springframework.validation.BindingResult; + +/** + * User: gkislin + * Date: 19.08.2014 + */ +public class ValidationException extends RuntimeException { + + public ValidationException(BindingResult result) { + super(getDescription(result)); + } + + private static String getDescription(BindingResult result) { + StringBuilder sb = new StringBuilder(); + result.getFieldErrors().forEach(fe -> sb.append(fe.getField()).append(" ").append(fe.getDefaultMessage()).append("
")); + return sb.toString(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java new file mode 100644 index 000000000000..03d1f96edf84 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.LoggerWrapper; +import ru.javawebinar.topjava.util.exception.ErrorInfo; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import ru.javawebinar.topjava.util.exception.ValidationException; + +import javax.servlet.http.HttpServletRequest; + +/** + * User: gkislin + * Date: 23.09.2014 + */ +@ControllerAdvice(annotations = RestController.class) +public class ExceptionInfoHandler { + private static final LoggerWrapper LOG = LoggerWrapper.get(ExceptionInfoHandler.class); + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(NotFoundException.class) + @ResponseBody + @Order(Ordered.HIGHEST_PRECEDENCE) + public ErrorInfo handleError(HttpServletRequest req, NotFoundException e) { + return LOG.getErrorInfo(req.getRequestURL(), e); + } + + @ResponseStatus(value = HttpStatus.CONFLICT) // 409 + @ExceptionHandler(DataIntegrityViolationException.class) + @ResponseBody + @Order(Ordered.HIGHEST_PRECEDENCE + 1) + public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { + return LOG.getErrorInfo(req.getRequestURL(), e); + } + + @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler(ValidationException.class) + @ResponseBody + @Order(Ordered.HIGHEST_PRECEDENCE + 2) + ErrorInfo validationError(HttpServletRequest req, ValidationException e) { + return LOG.getErrorInfo(req.getRequestURL(), e); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + @ResponseBody + @Order(Ordered.LOWEST_PRECEDENCE) + public ErrorInfo handleError(HttpServletRequest req, Exception e) { + return LOG.getErrorInfo(req.getRequestURL(), e); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java new file mode 100644 index 000000000000..b633bda11fc2 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalControllerExceptionHandler.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava.web; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; +import ru.javawebinar.topjava.LoggedUser; +import ru.javawebinar.topjava.LoggerWrapper; + +import javax.servlet.http.HttpServletRequest; + +/** + * User: gkislin + * Date: 23.09.2014 + */ +@ControllerAdvice +public class GlobalControllerExceptionHandler { + private static final LoggerWrapper LOG = LoggerWrapper.get(GlobalControllerExceptionHandler.class); + + + @ExceptionHandler(Exception.class) + @Order(Ordered.LOWEST_PRECEDENCE) + ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { + LOG.error("Exception at request " + req.getRequestURL()); + ModelAndView mav = new ModelAndView("exception/exception"); + mav.addObject("exception", e); + LoggedUser loggedUser = LoggedUser.safeGet(); + + // Interceptor is not invoked, put userTo + if (loggedUser != null) { + mav.addObject("userTo", loggedUser.getUserTo()); + } + return mav; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java new file mode 100644 index 000000000000..bf4b465dbbd6 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -0,0 +1,96 @@ +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; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.support.SessionStatus; +import ru.javawebinar.topjava.LoggedUser; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; +import ru.javawebinar.topjava.web.user.AbstractUserController; + +import javax.validation.Valid; + +/** + * User: gkislin + * Date: 22.08.2014 + */ +@Controller +public class RootController extends AbstractUserController { + + @RequestMapping(value = "/", method = RequestMethod.GET) + public String root() { + return "redirect:meals"; + } + + // @Secured("ROLE_ADMIN") + @PreAuthorize("hasRole('ROLE_ADMIN')") + @RequestMapping(value = "/users", method = RequestMethod.GET) + public String userList() { + return "userList"; + } + + @RequestMapping(value = "/meals", method = RequestMethod.GET) + public String mealList() { + return "mealList"; + } + + @RequestMapping(value = "/login", method = RequestMethod.GET) + public String login(ModelMap model, + @RequestParam(value = "error", required = false) boolean error, + @RequestParam(value = "message", required = false) String message) { + + model.put("error", error); + model.put("message", message); + return "login"; + } + + @RequestMapping(value = "/profile", method = RequestMethod.GET) + public String profile() { + return "profile"; + } + + @RequestMapping(value = "/profile", method = RequestMethod.POST) + public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status) { + if (!result.hasErrors()) { + try { + userTo.setId(LoggedUser.id()); + super.update(userTo); + LoggedUser.get().update(userTo); + status.setComplete(); + return "redirect:meals"; + } catch (DataIntegrityViolationException ex) { + result.rejectValue("email", "error.user", "user with this email already present in application"); + } + } + return "profile"; + } + + @RequestMapping(value = "/register", method = RequestMethod.GET) + public String register(ModelMap model) { + model.addAttribute("userTo", new UserTo()); + model.addAttribute("register", true); + return "profile"; + } + + @RequestMapping(value = "/register", method = RequestMethod.POST) + public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) { + if (!result.hasErrors()) { + try { + super.create(UserUtil.createFromTo(userTo)); + status.setComplete(); + return "redirect:login?message=app.registered"; + } catch (DataIntegrityViolationException ex) { + result.rejectValue("email", "error.user", "user with this email already present in application"); + } + } + model.addAttribute("register", true); + return "profile"; + } + +} diff --git a/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java new file mode 100644 index 000000000000..84f99195eae2 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.web.interceptor; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import ru.javawebinar.topjava.LoggedUser; + +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() && modelAndView.getModelMap().get("user") == null) { + LoggedUser loggedUser = LoggedUser.safeGet(); + if (loggedUser != null) { + modelAndView.getModelMap().addAttribute("userTo", loggedUser.getUserTo()); + } + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java new file mode 100644 index 000000000000..f2c5abb268e2 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JacksonObjectMapper.java @@ -0,0 +1,43 @@ +package ru.javawebinar.topjava.web.json; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module; + +import java.time.LocalDateTime; + +/** + * User: gkislin + * Date: 26.05.2014 + *

+ * Handling Hibernate lazy-loading + * + * @link https://github.com/FasterXML/jackson + * @link https://github.com/FasterXML/jackson-datatype-hibernate + * @link http://wiki.fasterxml.com/JacksonHowToCustomSerializers + */ +public class JacksonObjectMapper extends ObjectMapper { + + private static final ObjectMapper MAPPER = new JacksonObjectMapper(); + + public static ObjectMapper getMapper() { + return MAPPER; + } + + private JacksonObjectMapper() { + registerModule(new Hibernate4Module()); + + SimpleModule customModule = new SimpleModule("customModule", new Version(1, 0, 0, null)); + customModule.addSerializer(new JsonLocalDateTimeConverter.UserSettingSerializer()); + customModule.addDeserializer(LocalDateTime.class, new JsonLocalDateTimeConverter.UserSettingDeserializer()); + registerModule(customModule); + + setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); + setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + setSerializationInclusion(JsonInclude.Include.NON_NULL); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonLocalDateTimeConverter.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonLocalDateTimeConverter.java new file mode 100644 index 000000000000..8ef6f9f007fe --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonLocalDateTimeConverter.java @@ -0,0 +1,41 @@ +package ru.javawebinar.topjava.web.json; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; + +/** + * GKislin + * 06.01.2015. + */ +public class JsonLocalDateTimeConverter { + public static class UserSettingSerializer extends JsonSerializer { + @Override + public void serialize(LocalDateTime ldt, JsonGenerator jgen, SerializerProvider provider) throws IOException { + jgen.writeString(ldt.toString()); + } + + @Override + public Class handledType() { + return LocalDateTime.class; + } + } + + public static class UserSettingDeserializer extends JsonDeserializer { + @Override + public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctx) throws IOException { + return LocalDateTime.parse(jp.getText()); + } + + @Override + public Class handledType() { + return LocalDateTime.class; + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java new file mode 100644 index 000000000000..4af3fe861ffb --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java @@ -0,0 +1,44 @@ +package ru.javawebinar.topjava.web.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectReader; +import ru.javawebinar.topjava.LoggerWrapper; + +import java.io.IOException; +import java.util.List; + +import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper; + +/** + * User: gkislin + * Date: 30.04.2014 + */ +public class JsonUtil { + + private static final LoggerWrapper LOG = LoggerWrapper.get(JsonUtil.class); + + public static List readValues(String json, Class clazz) { + ObjectReader reader = getMapper().reader(clazz); + try { + return reader.readValues(json).readAll(); + } catch (IOException e) { + throw LOG.getIllegalArgumentException("Invalid read array from JSON:\n'" + json + "'", e); + } + } + + public static T readValue(String json, Class clazz) { + try { + return getMapper().readValue(json, clazz); + } catch (IOException e) { + throw LOG.getIllegalArgumentException("Invalid read from JSON:\n'" + json + "'", e); + } + } + + public static String writeValue(T obj) { + try { + return getMapper().writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e); + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractUserMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractUserMealController.java new file mode 100644 index 000000000000..ea6bd2a836a0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractUserMealController.java @@ -0,0 +1,66 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import ru.javawebinar.topjava.LoggedUser; +import ru.javawebinar.topjava.LoggerWrapper; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.service.UserMealService; +import ru.javawebinar.topjava.to.UserMealWithExceed; +import ru.javawebinar.topjava.util.UserMealsUtil; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +/** + * GKislin + * 06.03.2015. + */ +@Controller +public class AbstractUserMealController { + protected static final LoggerWrapper LOG = LoggerWrapper.get(AbstractUserMealController.class); + + @Autowired + protected UserMealService service; + + public UserMeal get(int id) { + int userId = LoggedUser.id(); + LOG.info("get meal {} for User {}", id, userId); + return service.get(id, userId); + } + + public void delete(int id) { + int userId = LoggedUser.id(); + LOG.info("delete meal {} for User {}", id, userId); + service.delete(id, userId); + } + + public List getAll() { + int userId = LoggedUser.id(); + LOG.info("getAll for User {}", userId); + return UserMealsUtil.getWithExceeded(service.getAll(userId), LoggedUser.getCaloriesPerDay()); + } + + public void update(UserMeal meal, int id) { + meal.setId(id); + int userId = LoggedUser.id(); + LOG.info("update {} for User {}", meal, userId); + service.update(meal, userId); + } + + public UserMeal create(UserMeal meal) { + meal.setId(null); + int userId = LoggedUser.id(); + LOG.info("create {} for User {}", meal, userId); + return service.save(meal, userId); + } + + public List getBetween(LocalDate startDate, LocalTime startTime, LocalDate endDate, LocalTime endTime) { + int userId = LoggedUser.id(); + LOG.info("getBetween dates {} - {} for time {} - {} for User {}", startDate, endDate, startTime, endTime, userId); + return UserMealsUtil.getFilteredWithExceeded( + service.getBetweenDates(startDate, endDate, userId), startTime, endTime, LoggedUser.getCaloriesPerDay() + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/UserMealAjaxController.java b/src/main/java/ru/javawebinar/topjava/web/meal/UserMealAjaxController.java new file mode 100644 index 000000000000..88727f2b9c27 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/UserMealAjaxController.java @@ -0,0 +1,61 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.http.MediaType; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.support.SessionStatus; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.to.UserMealWithExceed; +import ru.javawebinar.topjava.util.TimeUtil; +import ru.javawebinar.topjava.util.exception.ValidationException; + +import javax.validation.Valid; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +/** + * GKislin + * 06.03.2015. + */ +@RestController +@RequestMapping(value = "/ajax/profile/meals") +public class UserMealAjaxController extends AbstractUserMealController { + + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + return super.getAll(); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public UserMeal get(@PathVariable("id") int id) { + return super.get(id); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public void delete(@PathVariable("id") int id) { + super.delete(id); + } + + @RequestMapping(method = RequestMethod.POST) + public void updateOrCreate(@Valid UserMeal meal, BindingResult result, SessionStatus status) { + if (result.hasErrors()) { + throw new ValidationException(result); + } + status.setComplete(); + if (meal.getId() == 0) { + super.create(meal); + } else { + super.update(meal, meal.getId()); + } + } + + @RequestMapping(value = "/filter", method = RequestMethod.POST, 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 != null ? startDate : TimeUtil.MIN_DATE, startTime != null ? startTime : LocalTime.MIN, + endDate != null ? endDate : TimeUtil.MAX_DATE, endTime != null ? endTime : LocalTime.MAX); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/UserMealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/UserMealRestController.java new file mode 100644 index 000000000000..bedb2675b002 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/UserMealRestController.java @@ -0,0 +1,74 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.to.UserMealWithExceed; +import ru.javawebinar.topjava.util.TimeUtil; + +import java.net.URI; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@RestController +@RequestMapping(value = UserMealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) +public class UserMealRestController extends AbstractUserMealController { + public static final String REST_URL = "/rest/profile/meals"; + + @RequestMapping(value = "/{id}", method = RequestMethod.GET) + public UserMeal get(@PathVariable("id") int id) { + return super.get(id); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public void delete(@PathVariable("id") int id) { + super.delete(id); + } + + @RequestMapping(method = RequestMethod.GET) + public List getAll() { + return super.getAll(); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE) + public void update(@RequestBody UserMeal meal, @PathVariable("id") int id) { + super.update(meal, id); + } + + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@RequestBody UserMeal meal) { + UserMeal created = super.create(meal); + + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setLocation(uriOfNewResource); + + return new ResponseEntity<>(created, httpHeaders, HttpStatus.CREATED); + } + + @RequestMapping(value = "/between", method = RequestMethod.GET) + public List getBetween( + @RequestParam(value = "startDateTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDateTime, + @RequestParam(value = "endDateTime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDateTime) { + return super.getBetween(startDateTime.toLocalDate(), startDateTime.toLocalTime(), endDateTime.toLocalDate(), endDateTime.toLocalTime()); + } + + @RequestMapping(value = "/filter", method = RequestMethod.GET) + 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 != null ? startDate : TimeUtil.MIN_DATE, startTime != null ? startTime : LocalTime.MIN, + endDate != null ? endDate : TimeUtil.MAX_DATE, endTime != null ? endTime : LocalTime.MAX); + } +} 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..91fe7b0cdbc0 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -0,0 +1,62 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.LoggerWrapper; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; + +import java.util.Collection; + +/** + * User: gkislin + */ +public abstract class AbstractUserController { + protected final LoggerWrapper LOG = LoggerWrapper.get(getClass()); + + @Autowired + private UserService service; + + public Collection 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) { + user.setId(null); + LOG.info("create " + user); + return service.save(UserUtil.prepareToSave(user)); + } + + public void delete(int id) { + LOG.info("delete " + id); + service.delete(id); + } + + public void update(User user, int id) { + user.setId(id); + LOG.info("update " + user); + service.update(UserUtil.prepareToSave(user)); + } + + public void update(UserTo userTo) { + LOG.info("update " + userTo); + service.update(userTo); + } + + public User getByMail(String email) { + LOG.info("getByEmail " + email); + return service.getByEmail(email); + } + + public void enable(int id, boolean enabled) { + LOG.info((enabled ? "enable " : "disable ") + id); + service.enable(id, enabled); + } +} 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..f9688f45801a --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminAjaxController.java @@ -0,0 +1,59 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.MediaType; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.support.SessionStatus; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; +import ru.javawebinar.topjava.util.exception.ValidationException; + +import javax.validation.Valid; +import java.util.Collection; + +/** + * User: javawebinar.topjava + */ +@RestController +@RequestMapping("/ajax/admin/users") +public class AdminAjaxController extends AbstractUserController { + + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getAll() { + return super.getAll(); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public User get(@PathVariable("id") int id) { + return super.get(id); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public void delete(@PathVariable("id") int id) { + super.delete(id); + } + + @RequestMapping(method = RequestMethod.POST) + public void createOrUpdate(@Valid UserTo userTo, BindingResult result, SessionStatus status) { + if (result.hasErrors()) { + throw new ValidationException(result); + } + status.setComplete(); + try { + if (userTo.getId() == 0) { + super.create(UserUtil.createFromTo(userTo)); + } else { + super.update(userTo); + } + } catch (DataIntegrityViolationException e) { + throw new DataIntegrityViolationException("user with this email already present in application"); + } + } + + @RequestMapping(value = "/{id}", method = RequestMethod.POST) + public void enabled(@PathVariable("id") int id, @RequestParam("enabled") boolean enabled) { + super.enable(id, enabled); + } +} 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..f0fe6eb9382f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -0,0 +1,59 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.model.User; + +import java.net.URI; +import java.util.Collection; + +/** + * GKislin + * 06.03.2015. + */ +@RestController +@RequestMapping(AdminRestController.REST_URL) +public class AdminRestController extends AbstractUserController { + static final String REST_URL = "/rest/admin/users"; + + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getAll() { + return super.getAll(); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public User get(@PathVariable("id") int id) { + return super.get(id); + } + + @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@RequestBody User user) { + User created = super.create(user); + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setLocation(uriOfNewResource); + + return new ResponseEntity<>(created, httpHeaders, HttpStatus.CREATED); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) + public void delete(@PathVariable("id") int id) { + super.delete(id); + } + + @RequestMapping(value = "/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE) + public void update(@RequestBody User user, @PathVariable("id") int id) { + super.update(user, id); + } + + @RequestMapping(value = "/by", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public User getByMail(@RequestParam("email") String email) { + return super.getByMail(email); + } +} 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..babfd1f0e6d1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -0,0 +1,41 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import ru.javawebinar.topjava.LoggedUser; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; + +/** + * GKislin + * 06.03.2015. + */ +@RestController +@RequestMapping(ProfileRestController.REST_URL) +public class ProfileRestController extends AbstractUserController { + public static final String REST_URL = "/rest/profile"; + + @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) + public User get() { + return super.get(LoggedUser.id()); + } + + @RequestMapping(method = RequestMethod.DELETE) + public void delete() { + super.delete(LoggedUser.id()); + } + + @RequestMapping(method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE) + public void update(@RequestBody UserTo userTo) { + userTo.setId(LoggedUser.id()); + super.update(userTo); + } + + @RequestMapping(value = "/text", method = RequestMethod.GET) + public String testUTF() { + return "Русский текст"; + } +} \ No newline at end of file diff --git a/src/main/resources/cache/ehcache.xml b/src/main/resources/cache/ehcache.xml new file mode 100644 index 000000000000..b0275e589a4a --- /dev/null +++ b/src/main/resources/cache/ehcache.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/src/main/resources/cache/ehcache.xsd b/src/main/resources/cache/ehcache.xsd new file mode 100644 index 000000000000..bfc19ddb1e7d --- /dev/null +++ b/src/main/resources/cache/ehcache.xsd @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/heroku.properties b/src/main/resources/db/heroku.properties new file mode 100644 index 000000000000..02667eefeade --- /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 diff --git a/src/main/resources/db/hsqldb.properties b/src/main/resources/db/hsqldb.properties new file mode 100644 index 000000000000..a9449359016a --- /dev/null +++ b/src/main/resources/db/hsqldb.properties @@ -0,0 +1,12 @@ +#database.url=jdbc:hsqldb:file:D:/temp/topjava +database.url=jdbc:hsqldb:mem:topjava + +database.username=sa +database.password= + +database.init=true + +jdbc.initLocation=initDB_hsql.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql new file mode 100644 index 000000000000..26f6ca2929d1 --- /dev/null +++ b/src/main/resources/db/initDB.sql @@ -0,0 +1,35 @@ +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(), + enabled BOOL DEFAULT TRUE, + calories_per_day INTEGER DEFAULT 2000 NOT NULL +); +CREATE UNIQUE INDEX unique_email 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, + description TEXT, + calories INT, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql new file mode 100644 index 000000000000..274115ff7027 --- /dev/null +++ b/src/main/resources/db/initDB_hsql.sql @@ -0,0 +1,36 @@ +DROP TABLE user_roles IF EXISTS; +DROP TABLE meals IF EXISTS; +DROP TABLE users IF EXISTS; +DROP SEQUENCE global_seq IF EXISTS; + +CREATE SEQUENCE GLOBAL_SEQ AS INTEGER START WITH 100000; + +CREATE TABLE users +( + id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, + name VARCHAR(255), + email VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + registered TIMESTAMP DEFAULT now(), + enabled BOOLEAN DEFAULT TRUE, + calories_per_day INTEGER DEFAULT 2000 NOT NULL +); +CREATE UNIQUE INDEX user_email_key ON USERS ( email ); + +CREATE TABLE user_roles +( + user_id INTEGER NOT NULL, + role VARCHAR(255), + CONSTRAINT user_roles_idx UNIQUE (user_id, role), + FOREIGN KEY ( user_id ) REFERENCES USERS ( id ) ON DELETE CASCADE +); + +CREATE TABLE meals +( + id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, + date_time TIMESTAMP, + description VARCHAR(255), + calories INT, + user_id INTEGER NOT NULL, + FOREIGN KEY ( user_id ) REFERENCES USERS ( id ) ON DELETE CASCADE +); diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql new file mode 100644 index 000000000000..6629ce2ae620 --- /dev/null +++ b/src/main/resources/db/populateDB.sql @@ -0,0 +1,27 @@ +DELETE FROM user_roles; +DELETE FROM meals; +DELETE FROM users; +ALTER SEQUENCE global_seq RESTART WITH 100000; + +-- password +INSERT INTO users (name, email, password, calories_per_day) +VALUES ('User', 'user@yandex.ru', '$2a$10$Sh0ZD2NFrzRRJJEKEWn8l.92ROEuzlVyzB9SV1AM8fdluPR0aC1ni', 2005); + +-- admin +INSERT INTO users (name, email, password, calories_per_day) +VALUES ('Admin', 'admin@gmail.com', '$2a$10$WejOLxVuXRpOgr4IlzQJ.eT4UcukNqHlAiOVZj1P/nmc8WbpMkiju', 1900); + +INSERT INTO user_roles (role, user_id) VALUES + ('ROLE_USER', 100000), + ('ROLE_ADMIN', 100001), + ('ROLE_USER', 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..c004d087582d --- /dev/null +++ b/src/main/resources/db/postgres.properties @@ -0,0 +1,14 @@ +#database.url=jdbc:postgresql://ec2-54-217-202-110.eu-west-1.compute.amazonaws.com:5432/dehm6lvm8bink0?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory +#database.username=wegxlfzjjgxaxy +#database.password=SSQyKKE_e93kiUCR-ehzMcKCxZ + +database.url=jdbc:postgresql://localhost:5432/topjava +database.username=topjava +database.password=topjavauser + +database.init=true + +jdbc.initLocation=initDB.sql +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties new file mode 100644 index 000000000000..f84fd2bb0c9e --- /dev/null +++ b/src/main/resources/db/tomcat.properties @@ -0,0 +1,7 @@ +database.init=false + +jdbc.initLocation=initDB.sql + +jpa.showSql=true +hibernate.format_sql=true +hibernate.use_sql_comments=true diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 000000000000..9cfb5f2ea330 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,38 @@ + + + + + true + + + + + + + ${TOPJAVA_ROOT}/log/topjava.log + + + UTF-8 + %date %-5level %logger{0} [%file:%line] %msg%n + + + + + + UTF-8 + %-5level %logger{0} [%file:%line] %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..acca9cdbbd30 --- /dev/null +++ b/src/main/resources/spring/spring-app.xml @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml new file mode 100644 index 000000000000..cea96f7a74d6 --- /dev/null +++ b/src/main/resources/spring/spring-db.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-mvc.xml b/src/main/resources/spring/spring-mvc.xml new file mode 100644 index 000000000000..149d0edbffd4 --- /dev/null +++ b/src/main/resources/spring/spring-mvc.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + text/plain;charset=UTF-8 + text/html;charset=UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml new file mode 100644 index 000000000000..3805bb1f6864 --- /dev/null +++ b/src/main/resources/spring/spring-security.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/spring-tools.xml b/src/main/resources/spring/spring-tools.xml new file mode 100644 index 000000000000..784de2d79e20 --- /dev/null +++ b/src/main/resources/spring/spring-tools.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/tomcat/context.xml b/src/main/resources/tomcat/context.xml new file mode 100644 index 000000000000..3765c4df8165 --- /dev/null +++ b/src/main/resources/tomcat/context.xml @@ -0,0 +1,49 @@ + + + + + + + + WEB-INF/web.xml + ${catalina.base}/conf/web.xml + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/exception/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp new file mode 100644 index 000000000000..1ee7c6a7761f --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/exception/exception.jsp @@ -0,0 +1,24 @@ +<%@page isErrorPage="true" contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + +

+
+
+

Application error:

+

${exception.message}

+ +
+
+ + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp new file mode 100644 index 000000000000..3094acd18202 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -0,0 +1,28 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp new file mode 100644 index 000000000000..c77f34af71a3 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp @@ -0,0 +1,11 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp new file mode 100644 index 000000000000..47a1892d9d6d --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -0,0 +1,19 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> + + + + + + + + <fmt:message key="app.title"/> + ${pageContext.request.requestURL} + + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/lang.jsp b/src/main/webapp/WEB-INF/jsp/fragments/lang.jsp new file mode 100644 index 000000000000..7243054da5ca --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/lang.jsp @@ -0,0 +1,15 @@ +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/login.jsp b/src/main/webapp/WEB-INF/jsp/login.jsp new file mode 100644 index 000000000000..92d9f2305a01 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/login.jsp @@ -0,0 +1,90 @@ + +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@taglib uri="http://www.springframework.org/tags" prefix="spring" %> +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + + + + +
+
+ +
+ ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message} +
+
+ +
+ +
+
+

+ +

User login: user@yandex.ru / password

+ +

Admin login: admin@gmail.com / admin

+ +

Register »

+ +

Стек технологий: 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. +
+
+ + + + diff --git a/src/main/webapp/WEB-INF/jsp/mealList.jsp b/src/main/webapp/WEB-INF/jsp/mealList.jsp new file mode 100644 index 000000000000..52d0734bb7ed --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/mealList.jsp @@ -0,0 +1,122 @@ +<%@ 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="form" uri="http://www.springframework.org/tags/form" %> + + + + + + + + + +
+
+
+

+ +
+ + +
+ + +
+ +
+ + + +
+ +
+
+
+ + +
+ +
+ + + +
+ +
+
+
+
+ +
+
+
+ + + + + + + + + + + +
DateDescriptionCalories
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/profile.jsp b/src/main/webapp/WEB-INF/jsp/profile.jsp new file mode 100644 index 000000000000..e2cc0affaec4 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/profile.jsp @@ -0,0 +1,42 @@ + +<%@page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="topjava" tagdir="/WEB-INF/tags" %> + + + + + + + +
+
+
+

${register ? 'Register new' : userTo.name.concat(' profile')}

+ +
+ + + + + + + +
+
+ +
+
+
+
+
+
+
+ + + + diff --git a/src/main/webapp/WEB-INF/jsp/userList.jsp b/src/main/webapp/WEB-INF/jsp/userList.jsp new file mode 100644 index 000000000000..95dc8ae72aa7 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/userList.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="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + + + + + + + +
+
+
+

+ +
+ + + + + + + + + + + + + + +
NameEmailRolesActiveRegistered
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/tags/inputField.tag b/src/main/webapp/WEB-INF/tags/inputField.tag new file mode 100644 index 000000000000..6f4f5ff5adc2 --- /dev/null +++ b/src/main/webapp/WEB-INF/tags/inputField.tag @@ -0,0 +1,23 @@ +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +<%@ attribute name="name" required="true" description="Name of corresponding property in bean object" %> +<%@ attribute name="label" required="true" description="Field label" %> +<%@ attribute name="inputType" required="false" description="Input type" %> + + + +
+ + +
+ + + + + +  ${status.errorMessage} +
+
+
\ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000000..ed36b518174b --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,88 @@ + + + Topjava + + + spring.profiles.default + postgres,datajpa + + + + contextConfigLocation + + classpath:spring/spring-app.xml + classpath:spring/spring-db.xml + + + + + + org.springframework.web.context.ContextLoaderListener + + + mvc-dispatcher + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + classpath:spring/spring-mvc.xml + + 1 + + + mvc-dispatcher + / + + + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + + + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + springSecurityFilterChain + /* + + + + + diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css new file mode 100644 index 000000000000..7aa0d26a0a7a --- /dev/null +++ b/src/main/webapp/resources/css/style.css @@ -0,0 +1,28 @@ +.normal { + color: green; +} + +.exceeded { + color: red; +} + +.footer { + padding-bottom: 15px; +} + +.error, .message { + padding: 10px; + margin-bottom: 20px; + 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; +} \ 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..f7414edb0739 --- /dev/null +++ b/src/main/webapp/resources/js/datatablesUtil.js @@ -0,0 +1,119 @@ +function makeEditable() { + form = $('#detailsForm'); + + $('#add').click(function () { + form.find(":input").val(""); + $('#id').val(0); + $('#editRow').modal(); + }); + + form.submit(function () { + save(); + return false; + }); + + $(document).ajaxError(function (event, jqXHR, options, jsExc) { + failNoty(event, jqXHR, options, jsExc); + }); + + var token = $("meta[name='_csrf']").attr("content"); + var header = $("meta[name='_csrf_header']").attr("content"); + $(document).ajaxSend(function(e, xhr, options) { + xhr.setRequestHeader(header, token); + }); +} + +function updateRow(id) { + $.get(ajaxUrl + id, function (data) { + $.each(data, function (key, value) { + form.find("input[name='" + key + "']").val(value); + }); + $('#editRow').modal(); + }); +} + +function deleteRow(id) { + $.ajax({ + url: ajaxUrl + id, + type: 'DELETE', + success: function () { + updateTable(); + successNoty('Deleted'); + } + }); +} + +function enable(chkbox, id) { + var enabled = chkbox.is(":checked"); + chkbox.closest('tr').css("text-decoration", enabled ? "none" : "line-through"); + $.ajax({ + url: ajaxUrl + id, + type: 'POST', + data: 'enabled=' + enabled, + success: function () { + successNoty(enabled ? 'Enabled' : 'Disabled'); + } + }); +} + +function updateTableByData(data) { + datatableApi.clear().rows.add(data).draw(); +} + +function save() { + $.ajax({ + type: "POST", + url: ajaxUrl, + data: form.serialize(), + success: function () { + $('#editRow').modal('hide'); + updateTable(); + successNoty('Saved'); + } + }); +} + +var failedNote; + +function closeNote() { + if (failedNote) { + failedNote.close(); + failedNote = undefined; + } +} + +function successNoty(text) { + closeNote(); + noty({ + text: text, + type: 'success', + layout: 'bottomRight', + timeout: true + }); +} + +function failNoty(event, jqXHR, options, jsExc) { + closeNote(); + var errorInfo = $.parseJSON(jqXHR.responseText); + + failedNote = noty({ + text: 'Failed: ' + jqXHR.statusText + "
" + errorInfo.cause + "
" + errorInfo.detail, + type: 'error', + layout: 'bottomRight' + }); +} + +function renderEditBtn(data, type, row) { + if (type == 'display') { + return 'Edit'; + } + return data; +} + +function renderDeleteBtn(data, type, row) { + if (type == 'display') { + return 'Delete'; + } + return data; +} + diff --git a/src/main/webapp/resources/js/mealDatatables.js b/src/main/webapp/resources/js/mealDatatables.js new file mode 100644 index 000000000000..7fd26342e0e2 --- /dev/null +++ b/src/main/webapp/resources/js/mealDatatables.js @@ -0,0 +1,106 @@ +var ajaxUrl = 'ajax/profile/meals/'; +var datatableApi; + +function updateTable() { + $.ajax({ + type: "POST", + url: ajaxUrl + 'filter', + data: $('#filter').serialize(), + success: function (data) { + updateTableByData(data); + } + }); + return false; +} + +$(function () { + datatableApi = $('#datatable').DataTable({ + "ajax": { + "url": ajaxUrl, + "dataSrc": "" + }, + "paging": false, + "info": true, + "columns": [ + { + "data": "dateTime", + "render": function (date, type, row) { + if (type == 'display') { + return date.replace('T', ' '); + } + return date; + } + }, + { + "data": "description" + }, + { + "data": "calories" + }, + { + "defaultContent": "", + "orderable": false, + "render": renderEditBtn + }, + { + "defaultContent": "", + "orderable": false, + "render": renderDeleteBtn + + } + ], + "order": [ + [ + 0, + "desc" + ] + ], + "createdRow": function (row, data, dataIndex) { + $(row).addClass(data.exceed ? 'exceeded' : 'normal'); + }, + "initComplete": function () { + $('#filter').submit(function () { + updateTable(); + return false; + }); + + var startDate = $('#startDate'); + var endDate = $('#endDate'); + startDate.datetimepicker({ + timepicker: false, + format: 'Y-m-d', + lang: 'ru', + formatDate: 'Y-m-d', + onShow: function (ct) { + this.setOptions({ + maxDate: endDate.val() ? endDate.val() : false + }) + } + }); + endDate.datetimepicker({ + timepicker: false, + format: 'Y-m-d', + lang: 'ru', + formatDate: 'Y-m-d', + onShow: function (ct) { + this.setOptions({ + minDate: startDate.val() ? startDate.val() : false + }) + } + }); + + $('.time-picker').datetimepicker({ + datepicker: false, + format: 'H:i', + lang: 'ru' + }); + + $('#dateTime').datetimepicker({ + format: 'Y-m-d\\TH:i', + lang: 'ru' + }); + + makeEditable(); + } + }); +}); diff --git a/src/main/webapp/resources/js/userDatatables.js b/src/main/webapp/resources/js/userDatatables.js new file mode 100644 index 000000000000..a3cdc5533627 --- /dev/null +++ b/src/main/webapp/resources/js/userDatatables.js @@ -0,0 +1,77 @@ +var ajaxUrl = 'ajax/admin/users/'; +var datatableApi; + +function updateTable() { + $.get(ajaxUrl, function (data) { + updateTableByData(data); + }); +} + +$(function () { + datatableApi = $('#datatable').DataTable({ + "ajax": { + "url": ajaxUrl, + "dataSrc": "" + }, + "paging": false, + "info": true, + "columns": [ + { + "data": "name" + }, + { + "data": "email", + "render": function (data, type, row) { + if (type == 'display') { + return '' + data + ''; + } + return data; + } + }, + { + "data": "roles" + }, + { + "data": "enabled", + "render": function (data, type, row) { + if (type == 'display') { + return ''; + } + return data; + } + }, + { + "data": "registered", + "render": function (date, type, row) { + if (type == 'display') { + var dateObject = new Date(date); + return '' + dateObject.toISOString().substring(0, 10) + ''; + } + return date; + } + }, + { + "orderable": false, + "sDefaultContent": "", + "render": renderEditBtn + }, + { + "orderable": false, + "defaultContent": "", + "render": renderDeleteBtn + } + ], + "order": [ + [ + 0, + "asc" + ] + ], + "createdRow": function (row, data, dataIndex) { + if (!data.enabled) { + $(row).css("text-decoration", "line-through"); + } + }, + "initComplete": makeEditable + }); +}); \ 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..9aaf9a55cf82 --- /dev/null +++ b/src/main/webapp/test.html @@ -0,0 +1,17 @@ + + + + + +
+
+
+
User list:
+
users
+
Meal list:
+
meals
+
+
+
+ + \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java new file mode 100644 index 000000000000..c38a13b59999 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -0,0 +1,46 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.TestUtil.ToStringModelMatcher; +import ru.javawebinar.topjava.matcher.ModelMatcher; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.to.UserMealWithExceed; + +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +import static java.time.LocalDateTime.of; +import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ; + +/** + * GKislin + * 13.03.2015. + */ +public class MealTestData { + + public static final ModelMatcher MATCHER = new ToStringModelMatcher<>(UserMeal.class); + public static final ModelMatcher MATCHER_WITH_EXCEED = new ToStringModelMatcher<>(UserMealWithExceed.class); + + public static final int MEAL1_ID = START_SEQ + 2; + public static final int ADMIN_MEAL_ID = START_SEQ + 8; + + public static final UserMeal MEAL1 = new UserMeal(MEAL1_ID, LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500); + public static final UserMeal MEAL2 = new UserMeal(MEAL1_ID + 1, LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000); + public static final UserMeal MEAL3 = new UserMeal(MEAL1_ID + 2, LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500); + public static final UserMeal MEAL4 = new UserMeal(MEAL1_ID + 3, LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 500); + public static final UserMeal MEAL5 = new UserMeal(MEAL1_ID + 4, LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 1000); + public static final UserMeal MEAL6 = new UserMeal(MEAL1_ID + 5, LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510); + public static final UserMeal ADMIN_MEAL = new UserMeal(ADMIN_MEAL_ID, LocalDateTime.of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510); + public static final UserMeal ADMIN_MEAL2 = new UserMeal(ADMIN_MEAL_ID + 1, LocalDateTime.of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500); + + public static final List USER_MEALS = Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1); + + public static UserMeal getCreated() { + return new UserMeal(null, of(2015, Month.JUNE, 1, 18, 0), "Созданный ужин", 300); + } + + public static UserMeal getUpdated() { + return new UserMeal(MEAL1_ID, MEAL1.getDateTime(), "Обновленный завтрак", 200); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java new file mode 100644 index 000000000000..8ed01490c140 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -0,0 +1,38 @@ +package ru.javawebinar.topjava; + +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.to.UserMealWithExceed; +import ru.javawebinar.topjava.web.meal.UserMealRestController; +import ru.javawebinar.topjava.web.user.AdminRestController; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Month; +import java.util.Arrays; +import java.util.List; + +/** + * User: gkislin + * Date: 22.08.2014 + */ +public class SpringMain { + public static void main(String[] args) { + // java 7 Automatic resource management + try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml","spring/mock.xml")) { + System.out.println(Arrays.toString(appCtx.getBeanDefinitionNames())); + AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); + System.out.println(adminUserController.create(new User(1, "userName", "email", "password", 2005, Role.ROLE_ADMIN))); + System.out.println(); + + UserMealRestController mealController = appCtx.getBean(UserMealRestController.class); + List filteredMealsWithExceeded = + mealController.getBetween( + LocalDate.of(2015, Month.MAY, 30), LocalTime.of(7, 0), + LocalDate.of(2015, Month.MAY, 31), LocalTime.of(11, 0)); + filteredMealsWithExceeded.forEach(System.out::println); + } + } +} diff --git a/src/test/java/ru/javawebinar/topjava/TestUtil.java b/src/test/java/ru/javawebinar/topjava/TestUtil.java new file mode 100644 index 000000000000..6c39e88a84cc --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java @@ -0,0 +1,45 @@ +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.request.RequestPostProcessor; +import ru.javawebinar.topjava.matcher.ModelMatcher; +import ru.javawebinar.topjava.model.User; + +import java.io.UnsupportedEncodingException; + +/** + * GKislin + * 05.01.2015. + */ +public class TestUtil { + + public static ResultActions print(ResultActions action) throws UnsupportedEncodingException { + System.out.println(getContent(action)); + return action; + } + + public static String getContent(ResultActions action) throws UnsupportedEncodingException { + return action.andReturn().getResponse().getContentAsString(); + } + + public static RequestPostProcessor userHttpBasic(User user) { + return SecurityMockMvcRequestPostProcessors.httpBasic(user.getEmail(), user.getPassword()); + } + + public static void authorize(User user) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword())); + } + + /** + * Compare entities using toString + */ + public static class ToStringModelMatcher extends ModelMatcher { + public ToStringModelMatcher(Class entityClass) { + super(Object::toString, entityClass); + } + } +} diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java new file mode 100644 index 000000000000..2b711e1477ca --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -0,0 +1,89 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.matcher.ModelMatcher; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.util.PasswordUtil; +import ru.javawebinar.topjava.util.UserUtil; + +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ; + +/** + * GKislin + * 24.09.2015. + */ +public class UserTestData { + private static final LoggerWrapper LOG = LoggerWrapper.get(UserTestData.class); + + public static final int USER_ID = START_SEQ; + public static final int ADMIN_ID = START_SEQ + 1; + + public static final User USER = new User(USER_ID, "User", "user@yandex.ru", "password", 2005, Role.ROLE_USER); + public static final User ADMIN = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", 1900, Role.ROLE_ADMIN, Role.ROLE_USER); + + public static final ModelMatcher MATCHER = new ModelMatcher<>(u -> ((u instanceof TestUser) ? (TestUser) u : new TestUser(u)), User.class); + + public static class TestUser extends User { + + public TestUser(User u) { + this(u.getId(), u.getName(), u.getEmail(), u.getPassword(), u.getCaloriesPerDay(), u.isEnabled(), u.getRoles()); + } + + public TestUser(String name, String email, String password, Role role, Role... roles) { + this(null, name, email, password, UserUtil.DEFAULT_CALORIES_PER_DAY, true, EnumSet.of(role, roles)); + } + + public TestUser(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Set roles) { + super(id, name, email, password, caloriesPerDay, enabled, roles); + } + + public User asUser() { + return UserUtil.prepareToSave(new User(this)); + } + + @Override + public String toString() { + return "User (" + + "id=" + id + + ", email=" + email + + ", name=" + name + + ", enabled=" + enabled + + ", password=" + password + + ", authorities=" + roles + + ')'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + TestUser that = (TestUser) o; + return comparePassword(this.password, that.password) + && Objects.equals(this.id, that.id) + && Objects.equals(this.name, that.name) + && Objects.equals(this.email, that.email) + && Objects.equals(this.caloriesPerDay, that.caloriesPerDay) + && Objects.equals(this.enabled, that.enabled) + && Objects.equals(this.roles, that.roles); + } + } + + private static boolean comparePassword(String rawPassword, String password) { + if (PasswordUtil.isEncoded(rawPassword)) { + LOG.warn("Expected password couldn't be compared with actual"); + } else if (!PasswordUtil.isMatch(rawPassword, password)) { + LOG.error("Password " + password + " doesn't match encoded " + password); + return false; + } + return true; + } +} diff --git a/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java b/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java new file mode 100644 index 000000000000..b7078c203d2c --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/matcher/ModelMatcher.java @@ -0,0 +1,84 @@ +package ru.javawebinar.topjava.matcher; + +import org.junit.Assert; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; +import ru.javawebinar.topjava.TestUtil; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; + +/** + * GKislin + * 06.01.2015. + * + * @param : entity + * @param : testEntity, converter result for compare + */ +public class ModelMatcher { + protected Function entityConverter; + protected Class entityClass; + + public ModelMatcher(Function entityConverter, Class entityClass) { + this.entityConverter = entityConverter; + this.entityClass = entityClass; + } + + private T fromJsonValue(String json) { + return JsonUtil.readValue(json, entityClass); + } + + private Collection fromJsonValues(String json) { + return JsonUtil.readValues(json, entityClass); + } + + public void assertEquals(T expected, T actual) { + Assert.assertEquals(entityConverter.apply(expected), entityConverter.apply(actual)); + } + + public void assertCollectionEquals(Collection expected, Collection actual) { + Assert.assertEquals(map(expected, entityConverter), map(actual, entityConverter)); + } + + public static List map(Collection collection, Function converter) { + return collection.stream().map(converter).collect(Collectors.toList()); + } + + public ResultMatcher contentMatcher(T expect) { + return content().string( + new TestMatcher(expect) { + @Override + protected boolean compare(T expected, String body) { + R actualForCompare = entityConverter.apply(fromJsonValue(body)); + R expectedForCompare = entityConverter.apply(expected); + return expectedForCompare.equals(actualForCompare); + } + }); + } + + public final ResultMatcher contentListMatcher(T... expected) { + return contentListMatcher(Arrays.asList(expected)); + } + + public final ResultMatcher contentListMatcher(List expected) { + return content().string(new TestMatcher>(expected) { + @Override + protected boolean compare(List expected, String actual) { + List actualList = map(fromJsonValues(actual), entityConverter); + List expectedList = map(expected, entityConverter); + return expectedList.equals(actualList); + } + }); + } + + public T fromJsonAction(ResultActions action) throws UnsupportedEncodingException { + return fromJsonValue(TestUtil.getContent(action)); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java b/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java new file mode 100644 index 000000000000..32d599020146 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/matcher/TestMatcher.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.matcher; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import ru.javawebinar.topjava.web.json.JsonUtil; + +/** + * GKislin + * 05.01.2015. + */ +abstract public class TestMatcher extends BaseMatcher { + protected T expected; + + public TestMatcher(T expected) { + this.expected = expected; + } + + @Override + public boolean matches(Object actual) { + return compare(expected, (String) actual); + } + + abstract protected boolean compare(T expected, String actual); + + @Override + public void describeTo(Description description) { + description.appendText(JsonUtil.writeValue(expected)); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserMealRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserMealRepositoryImpl.java new file mode 100644 index 000000000000..73082554e58e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserMealRepositoryImpl.java @@ -0,0 +1,85 @@ +package ru.javawebinar.topjava.repository.mock; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.repository.UserMealRepository; +import ru.javawebinar.topjava.util.TimeUtil; + +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +/** + * GKislin + * 15.09.2015. + */ +@Repository +public class InMemoryUserMealRepositoryImpl implements UserMealRepository { + + public static final Comparator USER_MEAL_COMPARATOR = (um1, um2) -> um2.getDateTime().compareTo(um1.getDateTime()); + + // Map userId -> (mealId-> meal) + private Map> repository = new ConcurrentHashMap<>(); + private AtomicInteger counter = new AtomicInteger(0); + + { + save(new UserMeal(LocalDateTime.of(2015, Month.MAY, 30, 10, 0), "Завтрак", 500), USER_ID); + save(new UserMeal(LocalDateTime.of(2015, Month.MAY, 30, 13, 0), "Обед", 1000), USER_ID); + save(new UserMeal(LocalDateTime.of(2015, Month.MAY, 30, 20, 0), "Ужин", 500), USER_ID); + save(new UserMeal(LocalDateTime.of(2015, Month.MAY, 31, 10, 0), "Завтрак", 1000), USER_ID); + save(new UserMeal(LocalDateTime.of(2015, Month.MAY, 31, 13, 0), "Обед", 500), USER_ID); + save(new UserMeal(LocalDateTime.of(2015, Month.MAY, 31, 20, 0), "Ужин", 510), USER_ID); + + save(new UserMeal(LocalDateTime.of(2015, Month.JUNE, 1, 14, 0), "Админ ланч", 510), ADMIN_ID); + save(new UserMeal(LocalDateTime.of(2015, Month.JUNE, 1, 21, 0), "Админ ужин", 1500), ADMIN_ID); + } + + @Override + public UserMeal save(UserMeal userMeal, int userId) { + Integer mealId = userMeal.getId(); + + if (userMeal.isNew()) { + mealId = counter.incrementAndGet(); + userMeal.setId(mealId); + } else if (get(mealId, userId) == null) { + return null; + } + Map userMeals = repository.computeIfAbsent(userId, ConcurrentHashMap::new); + userMeals.put(mealId, userMeal); + return userMeal; + + } + + @Override + public boolean delete(int id, int userId) { + Map userMeals = repository.get(userId); + return userMeals != null && userMeals.remove(id) != null; + } + + @Override + public UserMeal get(int id, int userId) { + Map userMeals = repository.get(userId); + return userMeals == null ? null : userMeals.get(id); + } + + @Override + public Collection getAll(int userId) { + return repository.get(userId).values().stream().sorted(USER_MEAL_COMPARATOR).collect(Collectors.toList()); + } + + @Override + public Collection getBetween(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { + return getAll(userId).stream() + .filter(um -> TimeUtil.isBetween(um.getDateTime(), startDateTime, endDateTime)) + .sorted(USER_MEAL_COMPARATOR) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java new file mode 100644 index 000000000000..41cc257e9485 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/repository/mock/InMemoryUserRepositoryImpl.java @@ -0,0 +1,63 @@ +package ru.javawebinar.topjava.repository.mock; + +import org.springframework.stereotype.Repository; +import ru.javawebinar.topjava.LoggerWrapper; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * GKislin + * 15.06.2015. + */ +@Repository +public class InMemoryUserRepositoryImpl implements UserRepository { + private static final LoggerWrapper LOG = LoggerWrapper.get(InMemoryUserRepositoryImpl.class); + + private Map repository = new ConcurrentHashMap<>(); + private AtomicInteger counter = new AtomicInteger(0); + + @PostConstruct + public void postConstruct() { + LOG.info("+++ PostConstruct"); + } + + @PreDestroy + public void preDestroy() { + LOG.info("+++ PreDestroy"); + } + + @Override + public User save(User user) { + if (user.isNew()) { + user.setId(counter.incrementAndGet()); + } + return repository.put(user.getId(), user); + } + + @Override + public boolean delete(int id) { + return repository.remove(id) != null; + } + + @Override + public User get(int id) { + return repository.get(id); + } + + @Override + public Collection getAll() { + return repository.values(); + } + + @Override + public User getByEmail(String email) { + return getAll().stream().filter(u -> u.getEmail().equals(email)).findFirst().orElse(null); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java new file mode 100644 index 000000000000..eb8f8e9ea70e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractJpaUserServiceTest.java @@ -0,0 +1,21 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.repository.JpaUtil; + +/** + * GKislin + * 07.04.2015. + */ +abstract public class AbstractJpaUserServiceTest extends AbstractUserServiceTest { + @SuppressWarnings("SpringJavaAutowiringInspection") + @Autowired + private JpaUtil jpaUtil; + + @Before + public void setUp() throws Exception { + super.setUp(); + jpaUtil.clear2ndLevelHibernateCache(); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java new file mode 100644 index 000000000000..2585beafa26a --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -0,0 +1,29 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static ru.javawebinar.topjava.Profiles.POSTGRES; + +/** + * User: gkislin + * Date: 10.08.2014 + */ +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-db.xml" +}) +@RunWith(SpringJUnit4ClassRunner.class) +@ActiveProfiles({POSTGRES}) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +abstract public class AbstractServiceTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); +} diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserMealServiceTest.java new file mode 100644 index 000000000000..c52e5a51a5be --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserMealServiceTest.java @@ -0,0 +1,102 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.Stopwatch; +import org.junit.runner.Description; +import org.springframework.beans.factory.annotation.Autowired; +import ru.javawebinar.topjava.LoggerWrapper; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.time.LocalDate; +import java.time.Month; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; +import static ru.javawebinar.topjava.UserTestData.USER_ID; + +abstract public class AbstractUserMealServiceTest extends AbstractServiceTest { + private static final LoggerWrapper LOG = LoggerWrapper.get(AbstractUserMealServiceTest.class); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + // http://stackoverflow.com/questions/14892125/what-is-the-best-practice-to-determine-the-execution-time-of-the-bussiness-relev + public Stopwatch stopwatch = new Stopwatch() { + private void logInfo(Description description, long nanos) { + LOG.info(String.format("+++ Test %s spent %d microseconds", + description.getMethodName(), TimeUnit.NANOSECONDS.toMicros(nanos))); + } + + @Override + protected void finished(long nanos, Description description) { + logInfo(description, nanos); + } + }; + + @Autowired + protected UserMealService service; + + @Test + public void testDelete() throws Exception { + service.delete(MealTestData.MEAL1_ID, USER_ID); + MATCHER.assertCollectionEquals(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2), service.getAll(USER_ID)); + } + + @Test + public void testDeleteNotFound() throws Exception { + thrown.expect(NotFoundException.class); + service.delete(MEAL1_ID, 1); + } + + @Test + public void testSave() throws Exception { + UserMeal created = getCreated(); + service.save(created, USER_ID); + MATCHER.assertCollectionEquals(Arrays.asList(created, MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1), service.getAll(USER_ID)); + } + + @Test + public void testGet() throws Exception { + UserMeal actual = service.get(ADMIN_MEAL_ID, ADMIN_ID); + MATCHER.assertEquals(ADMIN_MEAL, actual); + } + + @Test + public void testGetNotFound() throws Exception { + thrown.expect(NotFoundException.class); + service.get(MEAL1_ID, ADMIN_ID); + } + + @Test + public void testUpdate() throws Exception { + UserMeal updated = getUpdated(); + service.update(updated, USER_ID); + MATCHER.assertEquals(updated, service.get(MEAL1_ID, USER_ID)); + } + + @Test + public void testNotFoundUpdate() throws Exception { + UserMeal item = service.get(MEAL1_ID, USER_ID); + thrown.expect(NotFoundException.class); + thrown.expectMessage("Not found entity with id=" + MEAL1_ID); + service.update(item, ADMIN_ID); + } + + @Test + public void testGetAll() throws Exception { + MATCHER.assertCollectionEquals(USER_MEALS, service.getAll(USER_ID)); + } + + @Test + public void testGetBetween() throws Exception { + MATCHER.assertCollectionEquals(Arrays.asList(MEAL3, MEAL2, MEAL1), + service.getBetweenDates(LocalDate.of(2015, Month.MAY, 30), LocalDate.of(2015, Month.MAY, 30), USER_ID)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java new file mode 100644 index 000000000000..74bccf3707b1 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -0,0 +1,84 @@ +package ru.javawebinar.topjava.service; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import ru.javawebinar.topjava.UserTestData.*; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static ru.javawebinar.topjava.UserTestData.*; + +abstract public class AbstractUserServiceTest extends AbstractServiceTest { + + @Autowired + protected UserService service; + + @Before + public void setUp() throws Exception { + service.evictCache(); + } + + @Test + public void testSave() throws Exception { + TestUser tu = new TestUser(null, "New", "new@gmail.com", "newPass", 1555, false, Collections.singleton(Role.ROLE_USER)); + User created = service.save(tu.asUser()); + tu.setId(created.getId()); + MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, tu, USER), service.getAll()); + } + + @Test(expected = DataAccessException.class) + public void testDuplicateMailSave() throws Exception { + service.save(new TestUser("Duplicate", "user@yandex.ru", "newPass", Role.ROLE_USER).asUser()); + } + + @Test + public void testDelete() throws Exception { + service.delete(USER_ID); + MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), service.getAll()); + } + + @Test(expected = NotFoundException.class) + public void testNotFoundDelete() throws Exception { + service.delete(1); + } + + @Test + public void testGet() throws Exception { + User user = service.get(USER_ID); + MATCHER.assertEquals(USER, user); + } + + @Test(expected = NotFoundException.class) + public void testGetNotFound() throws Exception { + service.get(1); + } + + @Test + public void testGetByEmail() throws Exception { + User user = service.getByEmail("user@yandex.ru"); + MATCHER.assertEquals(USER, user); + + } + + @Test + public void testGetAll() throws Exception { + Collection all = service.getAll(); + MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, USER), all); + } + + @Test + public void testUpdate() throws Exception { + TestUser updated = new TestUser(USER); + updated.setName("UpdatedName"); + updated.setCaloriesPerDay(330); + service.update(updated.asUser()); + MATCHER.assertEquals(updated, service.get(USER_ID)); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserMealServiceTest.java new file mode 100644 index 000000000000..c21c2ff095a9 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserMealServiceTest.java @@ -0,0 +1,21 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.Test; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.service.AbstractUserMealServiceTest; + +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.ADMIN_ID; + +@ActiveProfiles({DATAJPA}) +public class DataJpaUserMealServiceTest extends AbstractUserMealServiceTest { + @Test + public void testGetWithUser() throws Exception { + UserMeal adminMeal = service.getWithUser(ADMIN_MEAL_ID, ADMIN_ID); + MATCHER.assertEquals(ADMIN_MEAL, adminMeal); + UserTestData.MATCHER.assertEquals(UserTestData.ADMIN, adminMeal.getUser()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java new file mode 100644 index 000000000000..a4abf4eaccc1 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.service.datajpa; + +import org.junit.Test; +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.UserTestData.*; + +@ActiveProfiles({DATAJPA}) +public class DataJpaUserServiceTest extends AbstractJpaUserServiceTest { + @Test + public void testGetWithUser() throws Exception { + User user = service.getWithMeals(USER_ID); + MATCHER.assertEquals(USER, user); + MealTestData.MATCHER.assertCollectionEquals(MealTestData.USER_MEALS, user.getMeals()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserMealServiceTest.java new file mode 100644 index 000000000000..63227052ee17 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles({JDBC}) +public class JdbcUserMealServiceTest extends AbstractUserMealServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java new file mode 100644 index 000000000000..3088279e7736 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jdbc/JdbcUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jdbc; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JDBC; + +@ActiveProfiles({JDBC}) +public class JdbcUserServiceTest extends AbstractUserServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserMealServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserMealServiceTest.java new file mode 100644 index 000000000000..9175b47fed4b --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserMealServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractUserMealServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles({JPA}) +public class JpaUserMealServiceTest extends AbstractUserMealServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java new file mode 100644 index 000000000000..bb3b106df0b9 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/service/jpa/JpaUserServiceTest.java @@ -0,0 +1,10 @@ +package ru.javawebinar.topjava.service.jpa; + +import org.springframework.test.context.ActiveProfiles; +import ru.javawebinar.topjava.service.AbstractJpaUserServiceTest; + +import static ru.javawebinar.topjava.Profiles.JPA; + +@ActiveProfiles({JPA}) +public class JpaUserServiceTest extends AbstractJpaUserServiceTest { +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java new file mode 100644 index 000000000000..0ceb320c4aa3 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -0,0 +1,66 @@ +package ru.javawebinar.topjava.web; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.CharacterEncodingFilter; +import ru.javawebinar.topjava.service.UserService; + +import javax.annotation.PostConstruct; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static ru.javawebinar.topjava.Profiles.DATAJPA; +import static ru.javawebinar.topjava.Profiles.POSTGRES; + +/** + * User: gkislin + * Date: 10.08.2014 + */ +@ContextConfiguration({ + "classpath:spring/spring-app.xml", + "classpath:spring/spring-mvc.xml", + "classpath:spring/spring-db.xml" +}) +@WebAppConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +@Transactional +@ActiveProfiles({POSTGRES, DATAJPA}) +abstract public class AbstractControllerTest { + + private static final CharacterEncodingFilter CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter(); + + static { + CHARACTER_ENCODING_FILTER.setEncoding("UTF-8"); + CHARACTER_ENCODING_FILTER.setForceEncoding(true); + } + + protected MockMvc mockMvc; + + @Autowired + protected UserService userService; + + @Autowired + private WebApplicationContext webApplicationContext; + + @PostConstruct + void postConstruct() { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .addFilter(CHARACTER_ENCODING_FILTER) + .apply(springSecurity()) + .build(); + } + + @Before + public void setUp() { + userService.evictCache(); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/web/AdminInMemoryTest.java b/src/test/java/ru/javawebinar/topjava/web/AdminInMemoryTest.java new file mode 100644 index 000000000000..b463fc5428dd --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/AdminInMemoryTest.java @@ -0,0 +1,55 @@ +package ru.javawebinar.topjava.web; + +import org.junit.*; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import ru.javawebinar.topjava.web.user.AdminRestController; + +import java.util.Arrays; +import java.util.Collection; + +import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER; + +public class AdminInMemoryTest { + private static ConfigurableApplicationContext appCtx; + private static AdminRestController controller; + + @BeforeClass + public static void beforeClass() { + appCtx = new ClassPathXmlApplicationContext("spring/spring-app.xml","spring/spring-mvc.xml", "spring/mock.xml"); + System.out.println("\n" + Arrays.toString(appCtx.getBeanDefinitionNames()) + "\n"); + controller = appCtx.getBean(AdminRestController.class); + } + + @Before + public void setUp() throws Exception { + UserRepository repository = appCtx.getBean(UserRepository.class); + repository.save(USER); + repository.save(ADMIN); + } + + @AfterClass + public static void afterClass() { +// May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed +// http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite +// appCtx.close(); + } + + @Test + public void testDelete() throws Exception { + controller.delete(UserTestData.USER_ID); + Collection users = controller.getAll(); + Assert.assertEquals(users.size(), 1); + Assert.assertEquals(users.iterator().next(), ADMIN); + } + + @Test(expected = NotFoundException.class) + public void testDeleteNotFound() throws Exception { + controller.delete(10); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java new file mode 100644 index 000000000000..3d2585a50973 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.web; + +import org.junit.Test; +import org.springframework.http.MediaType; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * GKislin + * 10.04.2015. + */ +public class ResourceControllerTest extends AbstractControllerTest { + + @Test + public void testResources() throws Exception { + mockMvc.perform(get("/resources/css/style.css")) + .andDo(print()) + .andExpect(content().contentType(MediaType.valueOf("text/css"))) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java new file mode 100644 index 000000000000..c48a1efdd060 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -0,0 +1,45 @@ +package ru.javawebinar.topjava.web; + +import org.junit.Test; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static ru.javawebinar.topjava.TestUtil.authorize; +import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER; + +/** + * https://github.com/JavaOPs/topjava + */ +public class RootControllerTest extends AbstractControllerTest { + + @Test + public void testUserList() throws Exception { + authorize(ADMIN); + mockMvc.perform(get("/users")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("userList")) + .andExpect(forwardedUrl("/WEB-INF/jsp/userList.jsp")); + } + + @Test + public void testUserListUnAuth() throws Exception { + authorize(USER); + mockMvc.perform(get("/users")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("exception/exception")) + .andExpect(forwardedUrl("/WEB-INF/jsp/exception/exception.jsp")); + } + + @Test + public void testMealList() throws Exception { + authorize(USER); + mockMvc.perform(get("/meals")) + .andDo(print()) + .andExpect(view().name("mealList")) + .andExpect(forwardedUrl("/WEB-INF/jsp/mealList.jsp")); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/SpringAdminInMemoryTest.java b/src/test/java/ru/javawebinar/topjava/web/SpringAdminInMemoryTest.java new file mode 100644 index 000000000000..47d2fc40afe7 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/SpringAdminInMemoryTest.java @@ -0,0 +1,53 @@ +package ru.javawebinar.topjava.web; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import ru.javawebinar.topjava.UserTestData; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import ru.javawebinar.topjava.web.user.AdminRestController; + +import java.util.Collection; + +import static ru.javawebinar.topjava.UserTestData.ADMIN; +import static ru.javawebinar.topjava.UserTestData.USER; + +/** + * GKislin + * 13.03.2015. + */ +@ContextConfiguration({"classpath:spring/spring-app.xml","classpath:spring/spring-mvc.xml", "classpath:spring/mock.xml"}) +@RunWith(SpringJUnit4ClassRunner.class) +public class SpringAdminInMemoryTest { + + @Autowired + private AdminRestController controller; + + @Autowired + private UserRepository repository; + + @Before + public void setUp() throws Exception { + repository.save(USER); + repository.save(ADMIN); + } + + @Test + public void testDelete() throws Exception { + controller.delete(UserTestData.USER_ID); + Collection users = controller.getAll(); + Assert.assertEquals(users.size(), 1); + Assert.assertEquals(users.iterator().next(), ADMIN); + } + + @Test(expected = NotFoundException.class) + public void testDeleteNotFound() throws Exception { + controller.delete(10); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java new file mode 100644 index 000000000000..d1a7589c42ea --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java @@ -0,0 +1,30 @@ +package ru.javawebinar.topjava.web.json; + +import org.junit.Test; +import ru.javawebinar.topjava.MealTestData; +import ru.javawebinar.topjava.model.UserMeal; + +import java.util.List; + +/** + * GKislin + * 22.07.2015. + */ +public class JsonUtilTest { + + @Test + public void testReadWriteValue() throws Exception { + String json = JsonUtil.writeValue(MealTestData.ADMIN_MEAL); + System.out.println(json); + UserMeal userMeal = JsonUtil.readValue(json, UserMeal.class); + MealTestData.MATCHER.assertEquals(MealTestData.ADMIN_MEAL, userMeal); + } + + @Test + public void testReadWriteValues() throws Exception { + String json = JsonUtil.writeValue(MealTestData.USER_MEALS); + System.out.println(json); + List userMeals = JsonUtil.readValues(json, UserMeal.class); + MealTestData.MATCHER.assertCollectionEquals(MealTestData.USER_MEALS, userMeals); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/UserMealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/UserMealRestControllerTest.java new file mode 100644 index 000000000000..219d32df23d7 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/meal/UserMealRestControllerTest.java @@ -0,0 +1,140 @@ +package ru.javawebinar.topjava.web.meal; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import ru.javawebinar.topjava.model.UserMeal; +import ru.javawebinar.topjava.service.UserMealService; +import ru.javawebinar.topjava.util.UserMealsUtil; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +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 ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.MealTestData.MATCHER; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.*; +import static ru.javawebinar.topjava.model.BaseEntity.START_SEQ; + +public class UserMealRestControllerTest extends AbstractControllerTest { + + public static final String REST_URL = UserMealRestController.REST_URL + '/'; + + @Autowired + private UserMealService service; + + @Test + public void testGet() throws Exception { + mockMvc.perform(get(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MATCHER.contentMatcher(ADMIN_MEAL)); + } + + @Test + public void testGetUnauth() throws Exception { + mockMvc.perform(get(REST_URL + MEAL1_ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void testGetNotFound() throws Exception { + mockMvc.perform(get(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(USER))) + .andExpect(status().isNotFound()); + } + + @Test + public void testDeleteNotFound() throws Exception { + mockMvc.perform(delete(REST_URL + ADMIN_MEAL_ID).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER))) + .andDo(print()) + .andExpect(status().isNotFound()); + } + + @Test + public void testDelete() throws Exception { + mockMvc.perform(delete(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER))) + .andExpect(status().isOk()); + MATCHER.assertCollectionEquals(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2), service.getAll(START_SEQ)); + } + + @Test + public void testUpdate() throws Exception { + UserMeal updated = getUpdated(); + + mockMvc.perform(put(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(updated)) + .with(userHttpBasic(USER))) + .andExpect(status().isOk()); + + assertEquals(updated, service.get(MEAL1_ID, START_SEQ)); + } + + @Test + public void testCreate() throws Exception { + UserMeal created = getCreated(); + ResultActions action = mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(created)) + .with(userHttpBasic(ADMIN))); + + UserMeal returned = MATCHER.fromJsonAction(action); + created.setId(returned.getId()); + + MATCHER.assertEquals(created, returned); + MATCHER.assertCollectionEquals(Arrays.asList(ADMIN_MEAL2, created, ADMIN_MEAL), service.getAll(ADMIN_ID)); + } + + @Test + public void testGetAll() throws Exception { + mockMvc.perform(get(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MATCHER_WITH_EXCEED.contentListMatcher(UserMealsUtil.getWithExceeded(USER_MEALS, USER.getCaloriesPerDay()))); + } + + @Test + public void testGetBetween() throws Exception { + mockMvc.perform(get(REST_URL + "between?startDateTime=2015-05-30T07:00&endDateTime=2015-05-31T11:00:00") + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(MATCHER_WITH_EXCEED.contentListMatcher( + UserMealsUtil.createWithExceed(MEAL4, true), + UserMealsUtil.createWithExceed(MEAL1, false))); + } + + @Test + public void testFilter() throws Exception { + mockMvc.perform(get(REST_URL + "filter?startDate=2015-05-30&startTime=07:00&endDate=2015-05-31&endTime=11:00") + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(MATCHER_WITH_EXCEED.contentListMatcher( + UserMealsUtil.createWithExceed(MEAL4, true), + UserMealsUtil.createWithExceed(MEAL1, false))); + } + + @Test + public void testFilterAll() throws Exception { + mockMvc.perform(get(REST_URL + "filter?startDate=&endTime=") + .with(userHttpBasic(USER))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(MATCHER_WITH_EXCEED.contentListMatcher( + UserMealsUtil.getWithExceeded(Arrays.asList(MEAL6, MEAL5, MEAL4, MEAL3, MEAL2, MEAL1), USER.getCaloriesPerDay()))); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java new file mode 100644 index 000000000000..3a3af986ed48 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -0,0 +1,113 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import ru.javawebinar.topjava.TestUtil; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.util.Arrays; +import java.util.Collections; + +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 ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.*; + +public class AdminRestControllerTest extends AbstractControllerTest { + + public static final String REST_URL = AdminRestController.REST_URL + '/'; + + @Test + public void testGet() throws Exception { + mockMvc.perform(get(REST_URL + ADMIN_ID) + .with(userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MATCHER.contentMatcher(ADMIN)); + } + + @Test + public void testGetNotFound() throws Exception { + mockMvc.perform(get(REST_URL + 1) + .with(TestUtil.userHttpBasic(ADMIN))) + .andExpect(status().isNotFound()) + .andDo(print()); + } + + @Test + public void testGetByEmail() throws Exception { + mockMvc.perform(get(REST_URL + "by?email=" + ADMIN.getEmail()) + .with(TestUtil.userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MATCHER.contentMatcher(ADMIN)); + } + + @Test + public void testDelete() throws Exception { + mockMvc.perform(delete(REST_URL + USER_ID).contentType(MediaType.APPLICATION_JSON) + .with(TestUtil.userHttpBasic(ADMIN))) + .andDo(print()) + .andExpect(status().isOk()); + MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), userService.getAll()); + } + + @Test + public void testDeleteNotFound() throws Exception { + mockMvc.perform(delete(REST_URL + 1) + .with(TestUtil.userHttpBasic(ADMIN))) + .andExpect(status().isNotFound()) + .andDo(print()); + } + + @Test + public void testGetUnauth() throws Exception { + mockMvc.perform(get(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(TestUtil.userHttpBasic(USER))) + .andExpect(status().isForbidden()); + } + + public void testUpdate() throws Exception { + User updated = new User(USER); + updated.setName("UpdatedName"); + updated.setRoles(Collections.singleton(Role.ROLE_ADMIN)); + mockMvc.perform(put(REST_URL + USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .with(TestUtil.userHttpBasic(ADMIN)) + .content(JsonUtil.writeValue(updated))) + .andExpect(status().isOk()); + + MATCHER.assertEquals(updated, userService.get(USER_ID)); + } + + @Test + public void testCreate() throws Exception { + TestUser expected = new TestUser("New", "new@gmail.com", "newPass", Role.ROLE_USER, Role.ROLE_ADMIN); + ResultActions action = mockMvc.perform(post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(TestUtil.userHttpBasic(ADMIN)) + .content(JsonUtil.writeValue(expected.asUser()))).andExpect(status().isCreated()); + + User returned = MATCHER.fromJsonAction(action); + expected.setId(returned.getId()); + + MATCHER.assertEquals(expected, returned); + MATCHER.assertCollectionEquals(Arrays.asList(ADMIN, expected, USER), userService.getAll()); + } + + @Test + public void testGetAll() throws Exception { + TestUtil.print(mockMvc.perform(get(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(TestUtil.userHttpBasic(ADMIN))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MATCHER.contentListMatcher(ADMIN, USER))); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java new file mode 100644 index 000000000000..dee6506d30a2 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -0,0 +1,59 @@ +package ru.javawebinar.topjava.web.user; + +import org.junit.Test; +import org.springframework.http.MediaType; +import ru.javawebinar.topjava.TestUtil; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import java.util.Collections; + +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 ru.javawebinar.topjava.UserTestData.*; + +public class ProfileRestControllerTest extends AbstractControllerTest { + + public static final String REST_URL = ProfileRestController.REST_URL + '/'; + + @Test + public void testGet() throws Exception { + TestUtil.print(mockMvc.perform(get(REST_URL) + .with(TestUtil.userHttpBasic(USER))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(MATCHER.contentMatcher(USER))); + } + + @Test + public void testGetUnauth() throws Exception { + mockMvc.perform(get(REST_URL).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void testDelete() throws Exception { + mockMvc.perform(delete(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(TestUtil.userHttpBasic(USER))) + .andExpect(status().isOk()); + MATCHER.assertCollectionEquals(Collections.singletonList(ADMIN), userService.getAll()); + } + + @Test + public void testUpdate() throws Exception { + UserTo updatedTo = new UserTo(0, "newName", "newEmail", "newPassword", 1500); + + mockMvc.perform(put(REST_URL).contentType(MediaType.APPLICATION_JSON) + .with(TestUtil.userHttpBasic(USER)) + .content(JsonUtil.writeValue(updatedTo))) + .andDo(print()) + .andExpect(status().isOk()); + + MATCHER.assertEquals(UserUtil.updateFromTo(new User(USER), updatedTo), userService.getByEmail("newemail")); + } +} \ No newline at end of file diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..ec1d4c3ec0e0 --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,24 @@ + + + + true + + + + + UTF-8 + %-5level %logger{0} - %msg%n + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/spring/mock.xml b/src/test/resources/spring/mock.xml new file mode 100644 index 000000000000..c7c9f62cc5d8 --- /dev/null +++ b/src/test/resources/spring/mock.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/system.properties b/system.properties new file mode 100644 index 000000000000..916c446bb77d --- /dev/null +++ b/system.properties @@ -0,0 +1 @@ +java.runtime.version=1.8 \ No newline at end of file