diff --git a/README.md b/README.md index 10f296158..7fd61b43a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[](https://app.codacy.com/gh/JavaWebinar/topjava/dashboard) +[](https://www.codacy.com/gh/JavaWebinar/topjava/dashboard) Java Enterprise Online Project =============================== @@ -11,13 +11,14 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - [Wiki](https://github.com/JavaOPs/topjava/wiki) - [Wiki Git](https://github.com/JavaOPs/topjava/wiki/Git) - [Wiki IDEA](https://github.com/JavaOPs/topjava/wiki/IDEA) -- [Демо разрабатываемого приложения](http://javaops-demo.ru/topjava) +- [Демо разрабатываемого приложения](http://topjava.herokuapp.com/) -### 29.01: Старт проекта -- Начало проверки [вступительного задания HW0](https://github.com/JavaOPs/topjava#-Домашнее-задание-hw0) +### 26.05: Старт проекта +- Начало проверки [вступительного задания](https://github.com/JavaOPs/topjava#-Домашнее-задание-hw0) -#### 02.02 Дедлайн на сдачу HW0 -### 05.02: 1-е занятие +#### 31.05 Дедлайн на сдачу HW0 +### 02.06: 1-е занятие +#### 03.06 Дедлайн подачи заявки на [дипломную программу](https://javaops.ru/view/register/diploma) - Разбор домашнего задания вступительного занятия (вместе с Optional) - Обзор используемых в проекте технологий. Интеграция ПО - Maven @@ -26,7 +27,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Уровни и зависимости логгирования. JMX - Домашнее задание 1-го занятия (HW1 + Optional) -### 12.02: 2-е занятие +### 09.06: 2-е занятие - Разбор домашнего задания HW1 + Optional - Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava - Слои приложения. Создание каркаса приложения @@ -34,7 +35,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Пояснения к HW2. Обработка Autowired - Домашнее задание (HW2 + Optional) -### 19.02: 3-е занятие +### 16.06: 3-е занятие - Разбор домашнего задания HW2 + Optional - Жизненный цикл Spring контекста - Тестирование через JUnit @@ -47,7 +48,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Логирование тестов - Домашнее задание (HW3 + Optional) -### 26.02: 4-е занятие +### 23.06: 4-е занятие - Разбор домашнего задания HW3 + Optional - Методы улучшения качества кода - Spring: инициализация и популирование DB @@ -57,7 +58,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Домашнее задание (HW4 + Optional) #### Начало выполнения [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md) -### 05.03: 5-е занятие +### 30.06: 5-е занятие - Обзор JDK 9/17. Миграция Topjava с 1.8 на 17 - Разбор вопросов - Разбор домашнего задания HW4 + Optional @@ -68,7 +69,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Spring кэш - Домашнее задание (HW5 + Optional) -### 12.03: 6-е занятие +### 07.07: 6-е занятие - Разбор домашнего задания HW5 + Optional - Кэш Hibernate - Spring Web @@ -81,7 +82,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + #### Большое ДЗ + выпускной проект + начинаем [курс BootJava](https://javaops.ru/view/bootjava) + подтягиваем "хвосты". -### 26.03: 7-е занятие +### 21.07: 7-е занятие - Разбор домашнего задания HW6 + Optional - Автогенерация DDL по модели - Тестирование Spring MVC @@ -92,7 +93,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Тестирование через SoapUi. UTF-8 - Домашнее задание (HW7 + Optional) -### 02.04: 8-е занятие +### 28.07: 8-е занятие - Разбор домашнего задания HW7 + Optional - WebJars. jQuery и JavaScript frameworks - Bootstrap @@ -101,7 +102,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Добавление Spring Security - Домашнее задание (HW8 + Optional) -### 09.04: 9-е занятие +### 04.08: 9-е занятие - Разбор домашнего задания HW8 + Optional - Spring Binding - Spring Validation @@ -113,7 +114,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Cookie. Session - Домашнее задание (HW9 + Optional) -### 16.04: 10-е занятие +### 11.08: 10-е занятие - Разбор домашнего задания HW10 + Optional - Кастомизация JSON (@JsonView) и валидации (groups) - Рефакторинг: jQuery конверторы и группы валидации по умолчанию @@ -126,22 +127,23 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Защита от межсайтовой подделки запросов (CSRF) - Домашнее задание (HW10) -### 23.04: 11-е занятие +### 18.08: 11-е занятие - Разбор домашнего задания HW10 + Optional - Локализация datatables, ошибок валидации - Защита от XSS (Cross Site Scripting) - Обработка ошибок 404 (NotFound) - Доступ к AuthorizedUser - Ограничение модификации пользователей -- Деплой приложения [на собственный выделенный сервер](https://github.com/JavaOPs/startup) -- Домашнее задание (HW11): сокрытия полей в Swagger -- Составление резюме. Собеседование. Разработка ПО. Возможные доработки приложения - -### 27.04: Миграция на Spring-Boot 3.5 -- Ревью вашего резюме +- Деплой [приложения в Heroku](http://topjava.herokuapp.com) +- Собеседование. Разработка ПО +- Возможные доработки приложения +- Домашнее задание по проекту: составление резюме + +### 22.08: Миграция на Spring-Boot - Основы Spring Boot. Spring Boot maven plugin - Lombok, база H2, ApplicationRunner - Spring Data REST + HATEOAS - Миграция приложения подсчета калорий на Spring Boot -### 11.05: Дедлайн на сдачу [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md) +### 11.09.22: Дедлайн на сдачу [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md) +### 21.09.22: Получение дипломов для участников [Дипломной программы](https://javaops.ru/view/register/diploma) diff --git a/config/Topjava-soapui-project.xml b/config/Topjava-soapui-project.xml index f9668d765..f4b00d936 100644 --- a/config/Topjava-soapui-project.xml +++ b/config/Topjava-soapui-project.xml @@ -386,6 +386,220 @@ + + + + + + + + + 0 + + data + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals + + No Authorization + + + + + + + + + + + application/json + + + + application/json;charset=UTF-8 + 201 + + meal:Response + + + + <xml-fragment/> + + + http://localhost:8080 + { + "dateTime": "2020-02-01T10:00", + "description": "Новый завтрак", + "calories": 777 + } + + http://localhost/topjava/rest/profile/meals + + No Authorization + + + + + + + + + + + + + + + application/json;charset=UTF-8 + 200 + + ns:Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals/100002 + + No Authorization + + + + + + + + + + + + + 200 + + data + + + application/json + + + + + 200 + + data + + + + 200 + + data + + + + <xml-fragment/> + + UTF-8 + http://localhost:8080 + { + "id": 100002, + "dateTime": "2020-01-30T10:00", + "description": "Обновленный завтрак", + "calories": 500 + } + + http://localhost/topjava/rest/profile/meals/100002 + + No Authorization + + + + + + + + + + + + + startDate + + QUERY + + + + + startTime + + QUERY + + + + + endDate + + QUERY + + + + + endTime + + QUERY + + + + + + + + + application/json;charset=UTF-8 + 200 + + Response + + + + <xml-fragment/> + + http://localhost:8080 + + http://localhost/topjava/rest/profile/meals/filter + + No Authorization + + + + + + + + + + + startDate + startTime + endDate + endTime + + + + diff --git a/config/curl.md b/config/curl.md new file mode 100644 index 000000000..fbc748aa0 --- /dev/null +++ b/config/curl.md @@ -0,0 +1,38 @@ +### curl samples (application deployed at application context `topjava`). +> For windows use `Git Bash` + +#### get All Users +`curl -s http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` + +#### get Users 100001 +`curl -s http://localhost:8080/topjava/rest/admin/users/100001 --user admin@gmail.com:admin` + +#### register User +`curl -s -i -X POST -d '{"name":"New User","email":"test@mail.ru","password":"test-password"}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile` + +#### get Profile +`curl -s http://localhost:8080/topjava/rest/profile --user test@mail.ru:test-password` + +#### get All Meals +`curl -s http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` + +#### get Meals 100003 +`curl -s http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` + +#### filter Meals +`curl -s "http://localhost:8080/topjava/rest/profile/meals/filter?startDate=2020-01-30&startTime=07:00:00&endDate=2020-01-31&endTime=11:00:00" --user user@yandex.ru:password` + +#### get Meals not found +`curl -s -v http://localhost:8080/topjava/rest/profile/meals/100008 --user user@yandex.ru:password` + +#### delete Meals +`curl -s -X DELETE http://localhost:8080/topjava/rest/profile/meals/100002 --user user@yandex.ru:password` + +#### create Meals +`curl -s -X POST -d '{"dateTime":"2020-02-01T12:00","description":"Created lunch","calories":300}' -H 'Content-Type:application/json;charset=UTF-8' http://localhost:8080/topjava/rest/profile/meals --user user@yandex.ru:password` + +#### update Meals +`curl -s -X PUT -d '{"dateTime":"2020-01-30T07:00", "description":"Updated breakfast", "calories":200}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` + +#### validate with Error +`curl -s -X POST -d '{}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/admin/users --user admin@gmail.com:admin` diff --git a/config/messages/app.properties b/config/messages/app.properties index 6b4d2d155..8dec4e48f 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -1,14 +1,25 @@ app.title=Calories management -app.home=Home -app.footer=Internship Spring 5/JPA Enterprise (Topjava) application +app.stackTitle=Application stack: +app.description=Java Enterprise project with registration/authorization and role-based access rights (USER, ADMIN). \ +Admin could create/edit/delete users, users - manage your profile and data (meals) via UI (AJAX) and REST with basic authorization. \ +Meals could be filtered by date and time. Meal record color depends on daily calories sum exceeding "Daily calorie limit" (editable user's profile parameter). \ +All REST interface covered with JUnit tests by Spring MVC Test и Spring Security Test. +app.footer=Spring 5/JPA Enterprise (Topjava) internship application app.login=Login as +app.profile=profile +app.register=Registration +app.registered=You are registered. Please Sign in. user.title=Users +user.edit=Edit user +user.add=Add user user.name=Name user.email=Email user.roles=Roles user.active=Active user.registered=Registered +user.password=Password +user.caloriesPerDay=Daily calorie limit meal.title=Meals meal.edit=Edit meal @@ -22,8 +33,14 @@ meal.description=Description meal.dateTime=Date/Time meal.calories=Calories +common.add=Add common.select=Select -common.delete=Delete -common.update=Update +common.deleted=Record deleted +common.saved=Record saved +common.enabled=Record enabled +common.disabled=Record disabled +common.errorStatus=Error status +common.appError=Application error +common.confirm=Are you sure? common.save=Save common.cancel=Cancel \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index 97f3e9f05..be85ad30e 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -1,18 +1,29 @@ app.title=Подсчет калорий -app.home=Главная +app.stackTitle=Стек технологий: +app.description=Java Enterprise проект с регистрацией/авторизацией и правами доступа на основе ролей (USER, ADMIN). \ +Администратор может создавать/редактировать/удалять пользователей, а пользователи - управлять своим профилем и данными (едой) через UI (по AJAX) и по REST интерфейсу с базовой авторизацией. \ +Возможна фильтрация еды по датам и времени. Цвет записи таблицы еды зависит от того, превышает ли сумма калорий за день норму (редактируемый параметр в профиле пользователя). \ +Весь REST интерфейс покрывается JUnit тестами, используя Spring MVC Test и Spring Security Test. app.footer=Приложение стажировки Spring 5/JPA Enterprise (Topjava) app.login=Зайти как +app.profile=профиль +app.register=Регистрация +app.registered=Вы зарегистрированы. Введите ваш логин/пароль. user.title=Пользователи +user.edit=Редактировать пользователя +user.add=Добавить пользователя user.name=Имя user.email=Почта user.roles=Роли user.active=Активный user.registered=Зарегистрирован +user.password=Пароль +user.caloriesPerDay=Норма калорий в день meal.title=Моя еда -meal.edit=Редактирование еды -meal.add=Добавление еды +meal.edit=Редактировать еду +meal.add=Добавить еду meal.filter=Отфильтровать meal.startDate=От даты (включая) meal.endDate=До даты (включая) @@ -22,8 +33,14 @@ meal.description=Описание meal.dateTime=Дата/Время meal.calories=Калории +common.add=Добавить common.select=Выбрать -common.delete=Удалить -common.update=Обновить +common.deleted=Запись удалена +common.saved=Запись сохранена +common.enabled=Запись активирована +common.disabled=Запись деактивирована +common.errorStatus=Статус ошибки +common.appError=Ошибка приложения +common.confirm=Вы уверены? common.save=Сохранить common.cancel=Отменить \ No newline at end of file diff --git a/pom.xml b/pom.xml index e19fe64e8..1a9225385 100644 --- a/pom.xml +++ b/pom.xml @@ -9,60 +9,71 @@ 1.0-SNAPSHOT Calories Management - https://javaops-demo.ru/topjava + http://topjava.herokuapp.com/ - 21 + 17 UTF-8 UTF-8 - 5.3.39 - 2.7.18 - 2.21.2 - 9.0.113 - - 5.6.15.Final - 6.2.5.Final - 3.0.1-b12 + 5.3.20 + 2.7.1 + 5.7.2 - - 3.10.8 + 2.13.3 + 9.0.64 - 1.5.20 - 2.0.17 + 1.2.11 + 1.7.36 - 42.7.8 + 42.4.0 + + + 5.6.9.Final + 6.2.3.Final + 3.0.1-b12 + + + 3.10.0 + - 5.14.1 - 3.27.6 - 3.0 + 5.8.2 + 3.23.1 + 2.2 + + + 4.6.1 + 3.6.0 + 2.5.20-1 + 3.1.4 + 1.11.4 topjava package - - org.apache.maven.plugins - maven-war-plugin - 3.4.0 - org.apache.maven.plugins maven-compiler-plugin - 3.14.1 + 3.8.1 ${java.version} ${java.version} + + org.apache.maven.plugins + maven-war-plugin + 3.3.2 + org.apache.maven.plugins maven-surefire-plugin - 3.5.4 + 2.22.2 -Dfile.encoding=UTF-8 @@ -73,7 +84,7 @@ org.codehaus.cargo cargo-maven3-plugin - 1.10.26 + 1.9.13 tomcat9x @@ -118,6 +129,7 @@ org.slf4j slf4j-api ${slf4j.version} + compile @@ -127,14 +139,6 @@ runtime - - - com.google.code.findbugs - annotations - 3.0.1 - compile - - javax.annotation javax.annotation-api @@ -152,6 +156,23 @@ ${spring-data-jpa.version} + + + org.springframework.security + spring-security-web + ${spring.security.version} + + + org.springframework.security + spring-security-config + ${spring.security.version} + + + org.springframework.security + spring-security-taglibs + ${spring.security.version} + + org.hibernate @@ -222,6 +243,50 @@ spring-webmvc + + + org.webjars + jquery + ${webjars-jquery.version} + + + org.webjars + bootstrap + ${webjars-bootstrap.version} + + + org.webjars + jquery + + + + org.webjars + popper.js + + + + + org.webjars + datatables + ${webjars-datatables.version} + + + org.webjars + jquery + + + + + org.webjars + datetimepicker + ${webjars-datetimepicker.version} + + + org.webjars.bower + noty + ${webjars-noty.version} + + com.fasterxml.jackson.core @@ -243,7 +308,7 @@ org.junit.jupiter junit-jupiter-engine - ${junit.version} + ${junit.jupiter.version} test @@ -258,6 +323,13 @@ spring-test test + + org.springframework.security + spring-security-test + ${spring.security.version} + test + + org.assertj assertj-core @@ -269,7 +341,7 @@ org.junit.platform junit-platform-launcher - 1.14.3 + 1.8.2 test @@ -281,7 +353,7 @@ org.hsqldb hsqldb - 2.7.4 + 2.6.1 diff --git a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java new file mode 100644 index 000000000..2126d687b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java @@ -0,0 +1,37 @@ +package ru.javawebinar.topjava; + +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; + +import java.io.Serial; + +public class AuthorizedUser extends org.springframework.security.core.userdetails.User { + @Serial + private static final long serialVersionUID = 1L; + + private UserTo userTo; + + public AuthorizedUser(User user) { + super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles()); + setTo(UserUtil.asTo(user)); + } + + public int getId() { + return userTo.id(); + } + + public void setTo(UserTo newTo) { + newTo.setPassword(null); + userTo = newTo; + } + + public UserTo getUserTo() { + return userTo; + } + + @Override + public String toString() { + return userTo.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/HasId.java b/src/main/java/ru/javawebinar/topjava/HasId.java new file mode 100644 index 000000000..2ba30235c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/HasId.java @@ -0,0 +1,19 @@ +package ru.javawebinar.topjava; + +import org.springframework.util.Assert; + +public interface HasId { + Integer getId(); + + void setId(Integer id); + + default boolean isNew() { + return getId() == null; + } + + // doesn't work for hibernate lazy proxy + default int id() { + Assert.notNull(getId(), "Entity must has id"); + return getId(); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/View.java b/src/main/java/ru/javawebinar/topjava/View.java new file mode 100644 index 000000000..62c332bcf --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/View.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava; + +import javax.validation.groups.Default; + +public class View { + public interface Persist extends Default {} +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index e1200c3a5..536c5c986 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,17 +1,16 @@ package ru.javawebinar.topjava.model; -import org.springframework.data.domain.Persistable; +import org.hibernate.Hibernate; import org.springframework.util.Assert; +import ru.javawebinar.topjava.HasId; import javax.persistence.*; -import static org.hibernate.proxy.HibernateProxyHelper.getClassWithoutInitializingProxy; - @MappedSuperclass // http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access @Access(AccessType.FIELD) //@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, isGetterVisibility = NONE, setterVisibility = NONE) -public abstract class AbstractBaseEntity implements Persistable { +public abstract class AbstractBaseEntity implements HasId { public static final int START_SEQ = 100000; @Id @@ -29,6 +28,7 @@ protected AbstractBaseEntity(Integer id) { this.id = id; } + @Override public void setId(Integer id) { this.id = id; } @@ -38,32 +38,25 @@ public Integer getId() { return id; } - // doesn't work for hibernate lazy proxy - public int id() { - Assert.notNull(id, "Entity must have id"); - return id; - } - - @Override - public boolean isNew() { - return getId() == null; - } - @Override public String toString() { - return getClass().getSimpleName() + ":" + getId(); + return getClass().getSimpleName() + ":" + id; } - // https://stackoverflow.com/a/78077907/548473 @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClassWithoutInitializingProxy(this) != getClassWithoutInitializingProxy(o)) return false; - return getId() != null && getId().equals(((AbstractBaseEntity) o).getId()); + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !getClass().equals(Hibernate.getClass(o))) { + return false; + } + AbstractBaseEntity that = (AbstractBaseEntity) o; + return id != null && id.equals(that.id); } @Override - public final int hashCode() { - return getClassWithoutInitializingProxy(this).hashCode(); + public int hashCode() { + return id == null ? 0 : id; } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 22417097d..ed5063121 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,8 +1,12 @@ package ru.javawebinar.topjava.model; +import com.fasterxml.jackson.annotation.JsonBackReference; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import org.hibernate.validator.constraints.Range; +import org.springframework.format.annotation.DateTimeFormat; +import ru.javawebinar.topjava.View; +import ru.javawebinar.topjava.util.DateTimeUtil; import javax.persistence.*; import javax.validation.constraints.NotBlank; @@ -23,7 +27,7 @@ // "m.description=:desc where m.id=:id and m.user.id=:userId") }) @Entity -@Table(name = "meal", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meal_unique_user_datetime_idx")}) +@Table(name = "meals", uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "date_time"}, name = "meals_unique_user_datetime_idx")}) public class Meal extends AbstractBaseEntity { public static final String ALL_SORTED = "Meal.getAll"; public static final String DELETE = "Meal.delete"; @@ -31,6 +35,7 @@ public class Meal extends AbstractBaseEntity { @Column(name = "date_time", nullable = false) @NotNull + @DateTimeFormat(pattern = DateTimeUtil.DATE_TIME_PATTERN) private LocalDateTime dateTime; @Column(name = "description", nullable = false) @@ -39,22 +44,20 @@ public class Meal extends AbstractBaseEntity { private String description; @Column(name = "calories", nullable = false) + @NotNull @Range(min = 10, max = 5000) - private int calories; + private Integer calories; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) -// @NotNull + @JsonBackReference + @NotNull(groups = View.Persist.class) private User user; public Meal() { } - public Meal(LocalDateTime dateTime, String description, int calories) { - this(null, dateTime, description, calories); - } - public Meal(Integer id, LocalDateTime dateTime, String description, int calories) { super(id); this.dateTime = dateTime; @@ -90,7 +93,7 @@ public void setDescription(String description) { this.description = description; } - public void setCalories(int calories) { + public void setCalories(Integer calories) { this.calories = calories; } diff --git a/src/main/java/ru/javawebinar/topjava/model/Role.java b/src/main/java/ru/javawebinar/topjava/model/Role.java index acb7a276f..27f3e5231 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Role.java +++ b/src/main/java/ru/javawebinar/topjava/model/Role.java @@ -1,6 +1,14 @@ package ru.javawebinar.topjava.model; -public enum Role { +import org.springframework.security.core.GrantedAuthority; + +public enum Role implements GrantedAuthority { USER, - ADMIN + ADMIN; + +// https://stackoverflow.com/a/19542316/548473 + @Override + public String getAuthority() { + return "ROLE_" + name(); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 3b83fd506..00e9d5265 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,5 +1,7 @@ package ru.javawebinar.topjava.model; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import com.fasterxml.jackson.annotation.JsonProperty; import org.hibernate.annotations.Cache; import org.hibernate.annotations.*; import org.hibernate.validator.constraints.Range; @@ -17,7 +19,7 @@ import javax.validation.constraints.Size; import java.util.*; -import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; +import static ru.javawebinar.topjava.util.UserUtil.DEFAULT_CALORIES_PER_DAY; @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @NamedQueries({ @@ -42,6 +44,8 @@ public class User extends AbstractNamedEntity { @Column(name = "password", nullable = false) @NotBlank @Size(min = 5, max = 128) + // https://stackoverflow.com/a/12505165/548473 + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; @Column(name = "enabled", nullable = false, columnDefinition = "bool default true") @@ -49,12 +53,13 @@ public class User extends AbstractNamedEntity { @Column(name = "registered", nullable = false, columnDefinition = "timestamp default now()", updatable = false) @NotNull + @JsonProperty(access = JsonProperty.Access.READ_ONLY) private Date registered = new Date(); @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @Enumerated(EnumType.STRING) - @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), - uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role")}) + @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), + uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_roles")}) @Column(name = "role") @ElementCollection(fetch = FetchType.EAGER) // @Fetch(FetchMode.SUBSELECT) @@ -70,7 +75,7 @@ public class User extends AbstractNamedEntity { @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") @OnDelete(action = OnDeleteAction.CASCADE) //https://stackoverflow.com/a/44988100/548473 -// @JsonIgnore + @JsonManagedReference private List meals; public User() { @@ -80,8 +85,8 @@ public User(User u) { this(u.id, u.name, u.email, u.password, u.caloriesPerDay, u.enabled, u.registered, u.roles); } - public User(Integer id, String name, String email, String password, Role... roles) { - this(id, name, email, password, DEFAULT_CALORIES_PER_DAY, true, new Date(), List.of(roles)); + public User(Integer id, String name, String email, String password, int caloriesPerDay, Role... roles) { + this(id, name, email, password, caloriesPerDay, true, new Date(), Arrays.asList((roles))); } public User(Integer id, String name, String email, String password, int caloriesPerDay, boolean enabled, Date registered, Collection roles) { @@ -146,6 +151,10 @@ public List getMeals() { return meals; } + public void setMeals(List meals) { + this.meals = meals; + } + @Override public String toString() { return "User{" + diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java index 775d314ed..82c80dd7b 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -30,7 +30,7 @@ public class JdbcMealRepository implements MealRepository { public JdbcMealRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) { this.insertMeal = new SimpleJdbcInsert(jdbcTemplate) - .withTableName("meal") + .withTableName("meals") .usingGeneratedKeyColumns("id"); this.jdbcTemplate = jdbcTemplate; @@ -54,7 +54,7 @@ public Meal save(Meal meal, int userId) { meal.setId(newId.intValue()); } else { if (namedParameterJdbcTemplate.update("" + - "UPDATE meal " + + "UPDATE meals " + " SET description=:description, calories=:calories, date_time=:date_time " + " WHERE id=:id AND user_id=:user_id", map) == 0) { return null; @@ -66,26 +66,26 @@ public Meal save(Meal meal, int userId) { @Override @Transactional public boolean delete(int id, int userId) { - return jdbcTemplate.update("DELETE FROM meal WHERE id=? AND user_id=?", id, userId) != 0; + return jdbcTemplate.update("DELETE FROM meals WHERE id=? AND user_id=?", id, userId) != 0; } @Override public Meal get(int id, int userId) { List meals = jdbcTemplate.query( - "SELECT * FROM meal WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId); + "SELECT * FROM meals WHERE id = ? AND user_id = ?", ROW_MAPPER, id, userId); return DataAccessUtils.singleResult(meals); } @Override public List getAll(int userId) { return jdbcTemplate.query( - "SELECT * FROM meal WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId); + "SELECT * FROM meals WHERE user_id=? ORDER BY date_time DESC", ROW_MAPPER, userId); } @Override public List getBetweenHalfOpen(LocalDateTime startDateTime, LocalDateTime endDateTime, int userId) { return jdbcTemplate.query( - "SELECT * FROM meal WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC", + "SELECT * FROM meals WHERE user_id=? AND date_time >= ? AND date_time < ? ORDER BY date_time DESC", ROW_MAPPER, userId, startDateTime, endDateTime); } } diff --git a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java index ca5dcc185..dda61a5f4 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -90,7 +90,7 @@ public List getAll() { List users = jdbcTemplate.query("SELECT * FROM users ORDER BY name, email", ROW_MAPPER); Map> map = new HashMap<>(); - jdbcTemplate.query("SELECT * FROM user_role", rs -> { + jdbcTemplate.query("SELECT * FROM user_roles", rs -> { map.computeIfAbsent(rs.getInt("user_id"), userId -> EnumSet.noneOf(Role.class)) .add(Role.valueOf(rs.getString("role"))); }); @@ -101,7 +101,7 @@ public List getAll() { private void insertRoles(User u) { Set roles = u.getRoles(); if (!CollectionUtils.isEmpty(roles)) { - jdbcTemplate.batchUpdate("INSERT INTO user_role (user_id, role) VALUES (?, ?)", roles, roles.size(), + jdbcTemplate.batchUpdate("INSERT INTO user_roles (user_id, role) VALUES (?, ?)", roles, roles.size(), (ps, role) -> { ps.setInt(1, u.id()); ps.setString(2, role.name()); @@ -110,12 +110,12 @@ private void insertRoles(User u) { } private void deleteRoles(User u) { - jdbcTemplate.update("DELETE FROM user_role WHERE user_id=?", u.getId()); + jdbcTemplate.update("DELETE FROM user_roles WHERE user_id=?", u.getId()); } private User setRoles(User u) { if (u != null) { - List roles = jdbcTemplate.queryForList("SELECT role FROM user_role WHERE user_id=?", Role.class, u.getId()); + List roles = jdbcTemplate.queryForList("SELECT role FROM user_roles WHERE user_id=?", Role.class, u.getId()); u.setRoles(roles); } return u; diff --git a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java index 6df9fd99f..300a920ae 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jpa/JpaMealRepository.java @@ -25,8 +25,10 @@ public Meal save(Meal meal, int userId) { if (meal.isNew()) { em.persist(meal); return meal; + } else if (get(meal.id(), userId) == null) { + return null; } - return get(meal.id(), userId) == null ? null : em.merge(meal); + return em.merge(meal); } @Override diff --git a/src/main/java/ru/javawebinar/topjava/service/MealService.java b/src/main/java/ru/javawebinar/topjava/service/MealService.java index 5f759a11a..5e08c9e5a 100644 --- a/src/main/java/ru/javawebinar/topjava/service/MealService.java +++ b/src/main/java/ru/javawebinar/topjava/service/MealService.java @@ -11,7 +11,7 @@ import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfDayOrMin; import static ru.javawebinar.topjava.util.DateTimeUtil.atStartOfNextDayOrMax; -import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; @Service public class MealService { @@ -23,11 +23,11 @@ public MealService(MealRepository repository) { } public Meal get(int id, int userId) { - return checkNotFound(repository.get(id, userId), id); + return checkNotFoundWithId(repository.get(id, userId), id); } public void delete(int id, int userId) { - checkNotFound(repository.delete(id, userId), id); + checkNotFoundWithId(repository.delete(id, userId), id); } public List getBetweenInclusive(@Nullable LocalDate startDate, @Nullable LocalDate endDate, int userId) { @@ -40,7 +40,7 @@ public List getAll(int userId) { public void update(Meal meal, int userId) { Assert.notNull(meal, "meal must not be null"); - checkNotFound(repository.save(meal, userId), meal.id()); + checkNotFoundWithId(repository.save(meal, userId), meal.id()); } public Meal create(Meal meal, int userId) { @@ -49,6 +49,6 @@ public Meal create(Meal meal, int userId) { } public Meal getWithUser(int id, int userId) { - return checkNotFound(repository.getWithUser(id, userId), id); + return checkNotFoundWithId(repository.getWithUser(id, userId), id); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/service/UserService.java b/src/main/java/ru/javawebinar/topjava/service/UserService.java index 88b790359..0946273f6 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -2,37 +2,51 @@ import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import ru.javawebinar.topjava.AuthorizedUser; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; import java.util.List; +import static ru.javawebinar.topjava.util.UserUtil.prepareToSave; import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; -@Service -public class UserService { +@Service("userService") +@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) +public class UserService implements UserDetailsService { private final UserRepository repository; + private final PasswordEncoder passwordEncoder; - public UserService(UserRepository repository) { + public UserService(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; + this.passwordEncoder = passwordEncoder; } @CacheEvict(value = "users", allEntries = true) public User create(User user) { Assert.notNull(user, "user must not be null"); - return repository.save(user); + return prepareAndSave(user); } @CacheEvict(value = "users", allEntries = true) public void delete(int id) { - checkNotFound(repository.delete(id), id); + checkNotFoundWithId(repository.delete(id), id); } public User get(int id) { - return checkNotFound(repository.get(id), id); + return checkNotFoundWithId(repository.get(id), id); } public User getByEmail(String email) { @@ -48,10 +62,39 @@ public List getAll() { @CacheEvict(value = "users", allEntries = true) public void update(User user) { Assert.notNull(user, "user must not be null"); - checkNotFound(repository.save(user), user.id()); +// checkNotFoundWithId : check works only for JDBC, disabled + prepareAndSave(user); + } + + @CacheEvict(value = "users", allEntries = true) + @Transactional + public void update(UserTo userTo) { + User user = get(userTo.id()); + prepareAndSave(UserUtil.updateFromTo(user, userTo)); + } + + @CacheEvict(value = "users", allEntries = true) + @Transactional + public void enable(int id, boolean enabled) { + User user = get(id); + user.setEnabled(enabled); + repository.save(user); // !! need only for JDBC implementation + } + + @Override + public AuthorizedUser loadUserByUsername(String email) throws UsernameNotFoundException { + User user = repository.getByEmail(email.toLowerCase()); + if (user == null) { + throw new UsernameNotFoundException("User " + email + " is not found"); + } + return new AuthorizedUser(user); + } + + private User prepareAndSave(User user) { + return repository.save(prepareToSave(user, passwordEncoder)); } public User getWithMeals(int id) { - return checkNotFound(repository.getWithMeals(id), id); + return checkNotFoundWithId(repository.getWithMeals(id), id); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/to/BaseTo.java b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java new file mode 100644 index 000000000..b7a7de6b7 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java @@ -0,0 +1,24 @@ +package ru.javawebinar.topjava.to; + +import ru.javawebinar.topjava.HasId; + +public abstract class BaseTo implements HasId { + protected Integer id; + + public BaseTo() { + } + + public BaseTo(Integer id) { + this.id = id; + } + + @Override + public Integer getId() { + return id; + } + + @Override + public void setId(Integer id) { + this.id = id; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/to/MealTo.java b/src/main/java/ru/javawebinar/topjava/to/MealTo.java index d14feae79..059f14a44 100644 --- a/src/main/java/ru/javawebinar/topjava/to/MealTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/MealTo.java @@ -1,9 +1,10 @@ package ru.javawebinar.topjava.to; +import java.beans.ConstructorProperties; import java.time.LocalDateTime; +import java.util.Objects; -public class MealTo { - private final Integer id; +public class MealTo extends BaseTo { private final LocalDateTime dateTime; @@ -13,18 +14,15 @@ public class MealTo { private final boolean excess; + @ConstructorProperties({"id", "dateTime", "description", "calories", "excess"}) public MealTo(Integer id, LocalDateTime dateTime, String description, int calories, boolean excess) { - this.id = id; + super(id); this.dateTime = dateTime; this.description = description; this.calories = calories; this.excess = excess; } - public Integer getId() { - return id; - } - public LocalDateTime getDateTime() { return dateTime; } @@ -41,6 +39,23 @@ public boolean isExcess() { return excess; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MealTo mealTo = (MealTo) o; + return calories == mealTo.calories && + excess == mealTo.excess && + Objects.equals(id, mealTo.id) && + Objects.equals(dateTime, mealTo.dateTime) && + Objects.equals(description, mealTo.description); + } + + @Override + public int hashCode() { + return Objects.hash(id, dateTime, description, calories, excess); + } + @Override public String toString() { return "MealTo{" + 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 000000000..6f63cfa59 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -0,0 +1,86 @@ +package ru.javawebinar.topjava.to; + +import org.hibernate.validator.constraints.Range; +import ru.javawebinar.topjava.util.UserUtil; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serial; +import java.io.Serializable; + +public class UserTo extends BaseTo implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + @NotBlank + @Size(min = 2, max = 100) + private String name; + + @Email + @NotBlank + @Size(max = 100) + private String email; + + @NotBlank + @Size(min = 5, max = 32, message = "length must be between 5 and 32 characters") + private String password; + + @Range(min = 10, max = 10000) + @NotNull + private Integer caloriesPerDay = UserUtil.DEFAULT_CALORIES_PER_DAY; + + public UserTo() { + } + + public UserTo(Integer id, String name, String email, String password, int caloriesPerDay) { + super(id); + this.name = name; + this.email = email; + this.password = password; + this.caloriesPerDay = caloriesPerDay; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setCaloriesPerDay(Integer caloriesPerDay) { + this.caloriesPerDay = caloriesPerDay; + } + + public Integer getCaloriesPerDay() { + return caloriesPerDay; + } + + @Override + public String toString() { + return "UserTo{" + + "id=" + id + + ", name='" + name + '\'' + + ", email='" + email + '\'' + + ", caloriesPerDay='" + caloriesPerDay + '\'' + + '}'; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 7c1ac9fec..1fb662b11 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -7,9 +7,11 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; public class DateTimeUtil { - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm"; + public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); // DB doesn't support LocalDate.MIN/MAX private static final LocalDateTime MIN_DATE = LocalDateTime.of(1, 1, 1, 0, 0); @@ -23,7 +25,7 @@ public static LocalDateTime atStartOfDayOrMin(LocalDate localDate) { } public static LocalDateTime atStartOfNextDayOrMax(LocalDate localDate) { - return localDate != null ? localDate.plusDays(1).atStartOfDay() : MAX_DATE; + return localDate != null ? localDate.plus(1, ChronoUnit.DAYS).atStartOfDay() : MAX_DATE; } public static String toString(LocalDateTime ldt) { diff --git a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java index 658671b9d..af07e74d3 100644 --- a/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/MealsUtil.java @@ -12,7 +12,6 @@ import java.util.stream.Collectors; public class MealsUtil { - public static final int DEFAULT_CALORIES_PER_DAY = 2000; private MealsUtil() { } @@ -38,7 +37,7 @@ private static List filterByPredicate(Collection meals, int calori .toList(); } - private static MealTo createTo(Meal meal, boolean excess) { + public static MealTo createTo(Meal meal, boolean excess) { return new MealTo(meal.getId(), meal.getDateTime(), meal.getDescription(), meal.getCalories(), excess); } } diff --git a/src/main/java/ru/javawebinar/topjava/util/UserUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java new file mode 100644 index 000000000..a0700dd49 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java @@ -0,0 +1,33 @@ +package ru.javawebinar.topjava.util; + +import org.springframework.security.crypto.password.PasswordEncoder; +import ru.javawebinar.topjava.model.Role; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; + +public class UserUtil { + + public static final int DEFAULT_CALORIES_PER_DAY = 2000; + + public static User createNewFromTo(UserTo userTo) { + return new User(null, userTo.getName(), userTo.getEmail().toLowerCase(), userTo.getPassword(), userTo.getCaloriesPerDay(), Role.USER); + } + + public static UserTo asTo(User user) { + return new UserTo(user.getId(), user.getName(), user.getEmail(), user.getPassword(), user.getCaloriesPerDay()); + } + + public static User updateFromTo(User user, UserTo userTo) { + user.setName(userTo.getName()); + user.setEmail(userTo.getEmail().toLowerCase()); + user.setCaloriesPerDay(userTo.getCaloriesPerDay()); + user.setPassword(userTo.getPassword()); + return user; + } + + public static User prepareToSave(User user, PasswordEncoder passwordEncoder) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setEmail(user.getEmail().toLowerCase()); + return user; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 52ba3b57e..6c91f95fd 100644 --- a/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -1,10 +1,17 @@ package ru.javawebinar.topjava.util; -import ru.javawebinar.topjava.model.AbstractBaseEntity; + +import org.springframework.core.NestedExceptionUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; +import org.springframework.validation.BindingResult; +import ru.javawebinar.topjava.HasId; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; import ru.javawebinar.topjava.util.exception.NotFoundException; import javax.validation.*; import java.util.Set; +import java.util.stream.Collectors; public class ValidationUtil { @@ -28,12 +35,12 @@ public static void validate(T bean) { } } - public static T checkNotFound(T object, int id) { - checkNotFound(object != null, id); + public static T checkNotFoundWithId(T object, int id) { + checkNotFoundWithId(object != null, id); return object; } - public static void checkNotFound(boolean found, int id) { + public static void checkNotFoundWithId(boolean found, int id) { checkNotFound(found, "id=" + id); } @@ -48,18 +55,33 @@ public static void checkNotFound(boolean found, String msg) { } } - public static void checkIsNew(AbstractBaseEntity entity) { - if (!entity.isNew()) { - throw new IllegalArgumentException(entity + " must be new (id=null)"); + public static void checkNew(HasId bean) { + if (!bean.isNew()) { + throw new IllegalRequestDataException(bean + " must be new (id=null)"); } } - public static void assureIdConsistent(AbstractBaseEntity entity, int id) { + public static void assureIdConsistent(HasId bean, int id) { // conservative when you reply, but accept liberally (http://stackoverflow.com/a/32728226/548473) - if (entity.isNew()) { - entity.setId(id); - } else if (entity.id() != id) { - throw new IllegalArgumentException(entity + " must be with id=" + id); + if (bean.isNew()) { + bean.setId(id); + } else if (bean.id() != id) { + throw new IllegalRequestDataException(bean + " must be with id=" + id); } } + + // https://stackoverflow.com/a/65442410/548473 + @NonNull + public static Throwable getRootCause(@NonNull Throwable t) { + Throwable rootCause = NestedExceptionUtils.getRootCause(t); + return rootCause != null ? rootCause : t; + } + + public static ResponseEntity getErrorResponse(BindingResult result) { + return ResponseEntity.unprocessableEntity().body( + result.getFieldErrors().stream() + .map(fe -> String.format("[%s] %s", fe.getField(), fe.getDefaultMessage())) + .collect(Collectors.joining("")) + ); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java new file mode 100644 index 000000000..d43323590 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -0,0 +1,13 @@ +package ru.javawebinar.topjava.util.exception; + +public class ErrorInfo { + private final String url; + private final ErrorType type; + private final String detail; + + public ErrorInfo(CharSequence url, ErrorType type, String detail) { + this.url = url.toString(); + this.type = type; + this.detail = detail; + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java new file mode 100644 index 000000000..c53a433bc --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java @@ -0,0 +1,8 @@ +package ru.javawebinar.topjava.util.exception; + +public enum ErrorType { + APP_ERROR, + DATA_NOT_FOUND, + DATA_ERROR, + VALIDATION_ERROR +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java b/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java new file mode 100644 index 000000000..2b144f91c --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/util/exception/IllegalRequestDataException.java @@ -0,0 +1,7 @@ +package ru.javawebinar.topjava.util.exception; + +public class IllegalRequestDataException extends RuntimeException { + public IllegalRequestDataException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java new file mode 100644 index 000000000..aa841a06f --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -0,0 +1,65 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import ru.javawebinar.topjava.util.ValidationUtil; +import ru.javawebinar.topjava.util.exception.ErrorInfo; +import ru.javawebinar.topjava.util.exception.ErrorType; +import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; +import ru.javawebinar.topjava.util.exception.NotFoundException; + +import javax.servlet.http.HttpServletRequest; + +import static ru.javawebinar.topjava.util.exception.ErrorType.*; + +@RestControllerAdvice(annotations = RestController.class) +@Order(Ordered.HIGHEST_PRECEDENCE + 5) +public class ExceptionInfoHandler { + private static Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); + + // http://stackoverflow.com/a/22358422/548473 + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) + @ExceptionHandler(NotFoundException.class) + public ErrorInfo handleError(HttpServletRequest req, NotFoundException e) { + return logAndGetErrorInfo(req, e, false, DATA_NOT_FOUND); + } + + @ResponseStatus(HttpStatus.CONFLICT) // 409 + @ExceptionHandler(DataIntegrityViolationException.class) + public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { + return logAndGetErrorInfo(req, e, true, DATA_ERROR); + } + + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) // 422 + @ExceptionHandler({IllegalRequestDataException.class, MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class}) + public ErrorInfo illegalRequestDataError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(Exception.class) + public ErrorInfo handleError(HttpServletRequest req, Exception e) { + return logAndGetErrorInfo(req, e, true, APP_ERROR); + } + + // https://stackoverflow.com/questions/538870/should-private-helper-methods-be-static-if-they-can-be-static + private static ErrorInfo logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType) { + Throwable rootCause = ValidationUtil.getRootCause(e); + if (logException) { + log.error(errorType + " at request " + req.getRequestURL(), rootCause); + } else { + log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); + } + return new ErrorInfo(req.getRequestURL(), errorType, rootCause.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java new file mode 100644 index 000000000..bd401d1c1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java @@ -0,0 +1,36 @@ +package ru.javawebinar.topjava.web; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.ModelAndView; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.util.ValidationUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(Exception.class) + public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { + log.error("Exception at request " + req.getRequestURL(), e); + Throwable rootCause = ValidationUtil.getRootCause(e); + + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; + ModelAndView mav = new ModelAndView("exception", + Map.of("exception", rootCause, "message", rootCause.toString(), "status", httpStatus)); + mav.setStatus(httpStatus); + + // Interceptor is not invoked, put userTo + AuthorizedUser authorizedUser = SecurityUtil.safeGet(); + if (authorizedUser != null) { + mav.addObject("userTo", authorizedUser.getUserTo()); + } + return mav; + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/RootController.java b/src/main/java/ru/javawebinar/topjava/web/RootController.java index 921462ca9..d42b390d5 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -2,53 +2,37 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import ru.javawebinar.topjava.service.MealService; -import ru.javawebinar.topjava.service.UserService; -import ru.javawebinar.topjava.util.MealsUtil; - -import javax.servlet.http.HttpServletRequest; @Controller public class RootController { private static final Logger log = LoggerFactory.getLogger(RootController.class); - @Autowired - private UserService userService; - - @Autowired - private MealService mealService; - @GetMapping("/") public String root() { log.info("root"); - return "index"; + return "redirect:meals"; } + // @Secured("ROLE_ADMIN") + @PreAuthorize("hasRole('ADMIN')") @GetMapping("/users") - public String getUsers(Model model) { + public String getUsers() { log.info("users"); - model.addAttribute("users", userService.getAll()); return "users"; } - @PostMapping("/users") - public String setUser(HttpServletRequest request) { - int userId = Integer.parseInt(request.getParameter("userId")); - log.info("setUser {}", userId); - SecurityUtil.setAuthUserId(userId); - return "redirect:meals"; + @GetMapping("/login") + public String login() { + log.info("login"); + return "login"; } @GetMapping("/meals") - public String getMeals(Model model) { + public String getMeals() { log.info("meals"); - model.addAttribute("meals", - MealsUtil.getTos(mealService.getAll(SecurityUtil.authUserId()), SecurityUtil.authUserCaloriesPerDay())); return "meals"; } } diff --git a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java index 4bad5863e..ac5b99f05 100644 --- a/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/SecurityUtil.java @@ -1,25 +1,34 @@ package ru.javawebinar.topjava.web; -import ru.javawebinar.topjava.model.AbstractBaseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import ru.javawebinar.topjava.AuthorizedUser; -import static ru.javawebinar.topjava.util.MealsUtil.DEFAULT_CALORIES_PER_DAY; +import static java.util.Objects.requireNonNull; public class SecurityUtil { - private static int id = AbstractBaseEntity.START_SEQ; - private SecurityUtil() { } - public static int authUserId() { - return id; + public static AuthorizedUser safeGet() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null) { + return null; + } + Object principal = auth.getPrincipal(); + return (principal instanceof AuthorizedUser) ? (AuthorizedUser) principal : null; } - public static void setAuthUserId(int id) { - SecurityUtil.id = id; + public static AuthorizedUser get() { + return requireNonNull(safeGet(), "No authorized user found"); + } + + public static int authUserId() { + return get().getUserTo().id(); } public static int authUserCaloriesPerDay() { - return DEFAULT_CALORIES_PER_DAY; + return get().getUserTo().getCaloriesPerDay(); } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java new file mode 100644 index 000000000..bc4409869 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java @@ -0,0 +1,39 @@ +package ru.javawebinar.topjava.web.converter; + +import org.springframework.format.Formatter; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; +import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; + +public class DateTimeFormatters { + public static class LocalDateFormatter implements Formatter { + + @Override + public LocalDate parse(String text, Locale locale) { + return parseLocalDate(text); + } + + @Override + public String print(LocalDate lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_DATE); + } + } + + public static class LocalTimeFormatter implements Formatter { + + @Override + public LocalTime parse(String text, Locale locale) { + return parseLocalTime(text); + } + + @Override + public String print(LocalTime lt, Locale locale) { + return lt.format(DateTimeFormatter.ISO_LOCAL_TIME); + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java new file mode 100644 index 000000000..4ee6eaef1 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/interceptor/ModelInterceptor.java @@ -0,0 +1,25 @@ +package ru.javawebinar.topjava.web.interceptor; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.web.SecurityUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * This interceptor adds userTo to the model of every requests + */ +public class ModelInterceptor implements HandlerInterceptor { + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + if (modelAndView != null && !modelAndView.isEmpty()) { + AuthorizedUser authorizedUser = SecurityUtil.safeGet(); + if (authorizedUser != null) { + modelAndView.getModelMap().addAttribute("userTo", authorizedUser.getUserTo()); + } + } + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java index fda04590d..d3bf1ac04 100644 --- a/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java +++ b/src/main/java/ru/javawebinar/topjava/web/json/JsonUtil.java @@ -1,10 +1,12 @@ package ru.javawebinar.topjava.web.json; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectReader; import java.io.IOException; import java.util.List; +import java.util.Map; import static ru.javawebinar.topjava.web.json.JacksonObjectMapper.getMapper; @@ -34,4 +36,14 @@ public static String writeValue(T obj) { throw new IllegalStateException("Invalid write to JSON:\n'" + obj + "'", e); } } + + public static String writeAdditionProps(T obj, String addName, Object addValue) { + return writeAdditionProps(obj, Map.of(addName, addValue)); + } + + public static String writeAdditionProps(T obj, Map addProps) { + Map map = getMapper().convertValue(obj, new TypeReference<>() {}); + map.putAll(addProps); + return writeValue(map); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java index 1931f998f..ec601c187 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java @@ -15,7 +15,7 @@ import java.util.List; import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; -import static ru.javawebinar.topjava.util.ValidationUtil.checkIsNew; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; public abstract class AbstractMealController { private final Logger log = LoggerFactory.getLogger(getClass()); @@ -44,7 +44,7 @@ public List getAll() { public Meal create(Meal meal) { int userId = SecurityUtil.authUserId(); log.info("create {} for user {}", meal, userId); - checkIsNew(meal); + checkNew(meal); return service.create(meal, userId); } diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java deleted file mode 100644 index 7e800f683..000000000 --- a/src/main/java/ru/javawebinar/topjava/web/meal/JspMealController.java +++ /dev/null @@ -1,70 +0,0 @@ -package ru.javawebinar.topjava.web.meal; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import ru.javawebinar.topjava.model.Meal; - -import javax.servlet.http.HttpServletRequest; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.temporal.ChronoUnit; -import java.util.Objects; - -import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalDate; -import static ru.javawebinar.topjava.util.DateTimeUtil.parseLocalTime; - -@Controller -@RequestMapping("/meals") -public class JspMealController extends AbstractMealController { - - @GetMapping("/delete") - public String delete(HttpServletRequest request) { - super.delete(getId(request)); - return "redirect:/meals"; - } - - @GetMapping("/update") - public String update(HttpServletRequest request, Model model) { - model.addAttribute("meal", super.get(getId(request))); - return "mealForm"; - } - - @GetMapping("/create") - public String create(Model model) { - model.addAttribute("meal", new Meal(LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES), "", 1000)); - return "mealForm"; - } - - @PostMapping - public String updateOrCreate(HttpServletRequest request) { - Meal meal = new Meal(LocalDateTime.parse(request.getParameter("dateTime")), - request.getParameter("description"), - Integer.parseInt(request.getParameter("calories"))); - - if (request.getParameter("id").isEmpty()) { - super.create(meal); - } else { - super.update(meal, getId(request)); - } - return "redirect:/meals"; - } - - @GetMapping("/filter") - public String getBetween(HttpServletRequest request, Model model) { - LocalDate startDate = parseLocalDate(request.getParameter("startDate")); - LocalDate endDate = parseLocalDate(request.getParameter("endDate")); - LocalTime startTime = parseLocalTime(request.getParameter("startTime")); - LocalTime endTime = parseLocalTime(request.getParameter("endTime")); - model.addAttribute("meals", super.getBetween(startDate, startTime, endDate, endTime)); - return "meals"; - } - - private int getId(HttpServletRequest request) { - String paramId = Objects.requireNonNull(request.getParameter("id")); - return Integer.parseInt(paramId); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index 7f0a3d089..af1da8138 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -1,7 +1,67 @@ package ru.javawebinar.topjava.web.meal; -import org.springframework.stereotype.Controller; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; -@Controller +import java.net.URI; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@RestController +@RequestMapping(value = MealRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) public class MealRestController extends AbstractMealController { -} + static final String REST_URL = "/rest/profile/meals"; + + @Override + @GetMapping("/{id}") + public Meal get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @Override + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void update(@RequestBody Meal meal, @PathVariable int id) { + super.update(meal, id); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createWithLocation(@RequestBody Meal meal) { + Meal created = super.create(meal); + + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL + "/{id}") + .buildAndExpand(created.getId()).toUri(); + + return ResponseEntity.created(uriOfNewResource).body(created); + } + + @GetMapping("/filter") + public List getBetween( + @RequestParam @Nullable LocalDate startDate, + @RequestParam @Nullable LocalTime startTime, + @RequestParam @Nullable LocalDate endDate, + @RequestParam @Nullable LocalTime endTime) { + return super.getBetween(startDate, startTime, endDate, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java new file mode 100644 index 000000000..67715ce7b --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java @@ -0,0 +1,65 @@ +package ru.javawebinar.topjava.web.meal; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; +import ru.javawebinar.topjava.util.ValidationUtil; + +import javax.validation.Valid; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +@RestController +@RequestMapping(value = "/profile/meals", produces = MediaType.APPLICATION_JSON_VALUE) +public class MealUIController extends AbstractMealController { + + @Override + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @GetMapping( "/{id}") + public Meal get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @PostMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity createOrUpdate(@Valid Meal meal, BindingResult result) { + if (result.hasErrors()) { + // TODO change to exception handler + return ValidationUtil.getErrorResponse(result); + } + if (meal.isNew()) { + super.create(meal); + } else { + super.update(meal, meal.getId()); + } + return ResponseEntity.ok().build(); + } + + @Override + @GetMapping("/filter") + public List getBetween( + @RequestParam @Nullable LocalDate startDate, + @RequestParam @Nullable LocalTime startTime, + @RequestParam @Nullable LocalDate endDate, + @RequestParam @Nullable LocalTime endTime) { + return super.getBetween(startDate, startTime, endDate, endTime); + } +} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java index f9e6441ba..532e17816 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -5,11 +5,13 @@ import org.springframework.beans.factory.annotation.Autowired; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; import java.util.List; import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; -import static ru.javawebinar.topjava.util.ValidationUtil.checkIsNew; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); @@ -27,9 +29,15 @@ public User get(int id) { return service.get(id); } + public User create(UserTo userTo) { + log.info("create {}", userTo); + checkNew(userTo); + return service.create(UserUtil.createNewFromTo(userTo)); + } + public User create(User user) { log.info("create {}", user); - checkIsNew(user); + checkNew(user); return service.create(user); } @@ -44,8 +52,24 @@ public void update(User user, int id) { service.update(user); } + public void update(UserTo userTo, int id) { + log.info("update {} with id={}", userTo, id); + assureIdConsistent(userTo, id); + service.update(userTo); + } + public User getByMail(String email) { log.info("getByEmail {}", email); return service.getByEmail(email); } + + public User getWithMeals(int id) { + log.info("getWithMeals {}", id); + return service.getWithMeals(id); + } + + public void enable(int id, boolean enabled) { + log.info(enabled ? "enable {}" : "disable {}", id); + service.enable(id, enabled); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java index 095ced3b0..dfc40e6c6 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -56,4 +56,16 @@ public void update(@RequestBody User user, @PathVariable int id) { public User getByMail(@RequestParam String email) { return super.getByMail(email); } + + @GetMapping("/{id}/with-meals") + public User getWithMeals(@PathVariable int id) { + return super.getWithMeals(id); + } + + @Override + @PatchMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void enable(@PathVariable int id, @RequestParam boolean enabled) { + super.enable(id, enabled); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java new file mode 100644 index 000000000..0f5218560 --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java @@ -0,0 +1,59 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.ValidationUtil; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequestMapping(value = "/admin/users", produces = MediaType.APPLICATION_JSON_VALUE) +public class AdminUIController extends AbstractUserController { + + @Override + @GetMapping + public List getAll() { + return super.getAll(); + } + + @Override + @GetMapping("/{id}") + public User get(@PathVariable int id) { + return super.get(id); + } + + @Override + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(@PathVariable int id) { + super.delete(id); + } + + @PostMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity createOrUpdate(@Valid UserTo userTo, BindingResult result) { + if (result.hasErrors()) { + // TODO change to exception handler + return ValidationUtil.getErrorResponse(result); + } + if (userTo.isNew()) { + super.create(userTo); + } else { + super.update(userTo, userTo.id()); + } + return ResponseEntity.ok().build(); + } + + @Override + @PostMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void enable(@PathVariable int id, @RequestParam boolean enabled) { + super.enable(id, enabled); + } +} diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java index 14559e4cf..c99f2ccaa 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -2,8 +2,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.to.UserTo; + +import java.net.URI; import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; @@ -23,14 +28,28 @@ public void delete() { super.delete(authUserId()); } + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity register(@RequestBody UserTo userTo) { + User created = super.create(userTo); + URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() + .path(REST_URL).build().toUri(); + return ResponseEntity.created(uriOfNewResource).body(created); + } + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) - public void update(@RequestBody User user) { - super.update(user, authUserId()); + public void update(@RequestBody UserTo userTo) { + super.update(userTo, authUserId()); } @GetMapping("/text") public String testUTF() { return "Русский текст"; } + + @GetMapping("/with-meals") + public User getWithMeals() { + return super.getWithMeals(authUserId()); + } } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java new file mode 100644 index 000000000..657396a7d --- /dev/null +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java @@ -0,0 +1,54 @@ +package ru.javawebinar.topjava.web.user; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.support.SessionStatus; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.web.SecurityUtil; + +import javax.validation.Valid; + +@Controller +@RequestMapping("/profile") +public class ProfileUIController extends AbstractUserController { + + @GetMapping + public String profile() { + return "profile"; + } + + @PostMapping + public String updateProfile(@Valid UserTo userTo, BindingResult result, SessionStatus status) { + if (result.hasErrors()) { + return "profile"; + } else { + super.update(userTo, SecurityUtil.authUserId()); + SecurityUtil.get().setTo(userTo); + status.setComplete(); + return "redirect:/meals"; + } + } + + @GetMapping("/register") + public String register(ModelMap model) { + model.addAttribute("userTo", new UserTo()); + model.addAttribute("register", true); + return "profile"; + } + + @PostMapping("/register") + public String saveRegister(@Valid UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) { + if (result.hasErrors()) { + model.addAttribute("register", true); + return "profile"; + } else { + super.create(userTo); + status.setComplete(); + return "redirect:/login?message=app.registered&username=" + userTo.getEmail(); + } + } +} \ No newline at end of file diff --git a/src/main/resources/db/initDB.sql b/src/main/resources/db/initDB.sql index 4bf3d8446..7644dc610 100644 --- a/src/main/resources/db/initDB.sql +++ b/src/main/resources/db/initDB.sql @@ -1,5 +1,5 @@ -DROP TABLE IF EXISTS user_role; -DROP TABLE IF EXISTS meal; +DROP TABLE IF EXISTS user_roles; +DROP TABLE IF EXISTS meals; DROP TABLE IF EXISTS users; DROP SEQUENCE IF EXISTS global_seq; @@ -17,7 +17,7 @@ CREATE TABLE users ); CREATE UNIQUE INDEX users_unique_email_idx ON users (email); -CREATE TABLE user_role +CREATE TABLE user_roles ( user_id INTEGER NOT NULL, role VARCHAR NOT NULL, @@ -25,7 +25,7 @@ CREATE TABLE user_role FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); -CREATE TABLE meal +CREATE TABLE meals ( id INTEGER PRIMARY KEY DEFAULT nextval('global_seq'), user_id INTEGER NOT NULL, @@ -34,4 +34,4 @@ CREATE TABLE meal calories INT NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); -CREATE UNIQUE INDEX meal_unique_user_datetime_idx ON meal (user_id, date_time); \ No newline at end of file +CREATE UNIQUE INDEX meals_unique_user_datetime_idx ON meals (user_id, date_time); \ No newline at end of file diff --git a/src/main/resources/db/initDB_hsql.sql b/src/main/resources/db/initDB_hsql.sql index 9e0e195e6..f2bb54b1e 100644 --- a/src/main/resources/db/initDB_hsql.sql +++ b/src/main/resources/db/initDB_hsql.sql @@ -1,5 +1,5 @@ -DROP TABLE user_role IF EXISTS; -DROP TABLE meal IF EXISTS; +DROP TABLE user_roles IF EXISTS; +DROP TABLE meals IF EXISTS; DROP TABLE users IF EXISTS; DROP SEQUENCE global_seq IF EXISTS; @@ -18,7 +18,7 @@ CREATE TABLE users CREATE UNIQUE INDEX users_unique_email_idx ON USERS (email); -CREATE TABLE user_role +CREATE TABLE user_roles ( user_id INTEGER NOT NULL, role VARCHAR(255) NOT NULL, @@ -26,7 +26,7 @@ CREATE TABLE user_role FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE ); -CREATE TABLE meal +CREATE TABLE meals ( id INTEGER GENERATED BY DEFAULT AS SEQUENCE GLOBAL_SEQ PRIMARY KEY, date_time TIMESTAMP NOT NULL, @@ -35,5 +35,5 @@ CREATE TABLE meal user_id INTEGER NOT NULL, FOREIGN KEY (user_id) REFERENCES USERS (id) ON DELETE CASCADE ); -CREATE UNIQUE INDEX meal_unique_user_datetime_idx - ON meal (user_id, date_time) \ No newline at end of file +CREATE UNIQUE INDEX meals_unique_user_datetime_idx + ON meals (user_id, date_time) \ No newline at end of file diff --git a/src/main/resources/db/populateDB.sql b/src/main/resources/db/populateDB.sql index 9e9bd828b..8265d3655 100644 --- a/src/main/resources/db/populateDB.sql +++ b/src/main/resources/db/populateDB.sql @@ -1,19 +1,19 @@ -DELETE FROM user_role; -DELETE FROM meal; +DELETE FROM user_roles; +DELETE FROM meals; DELETE FROM users; ALTER SEQUENCE global_seq RESTART WITH 100000; -INSERT INTO users (name, email, password) -VALUES ('User', 'user@yandex.ru', 'password'), - ('Admin', 'admin@gmail.com', 'admin'), - ('Guest', 'guest@gmail.com', 'guest'); +INSERT INTO users (name, email, password, calories_per_day) +VALUES ('User', 'user@yandex.ru', '{noop}password', 2005), + ('Admin', 'admin@gmail.com', '{noop}admin', 1900), + ('Guest', 'guest@gmail.com', '{noop}guest', 2000); -INSERT INTO user_role (role, user_id) +INSERT INTO user_roles (role, user_id) VALUES ('USER', 100000), ('ADMIN', 100001), ('USER', 100001); -INSERT INTO meal (date_time, description, calories, user_id) +INSERT INTO meals (date_time, description, calories, user_id) VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000), ('2020-01-30 13:00:00', 'Обед', 1000, 100000), ('2020-01-30 20:00:00', 'Ужин', 500, 100000), @@ -22,4 +22,4 @@ VALUES ('2020-01-30 10:00:00', 'Завтрак', 500, 100000), ('2020-01-31 13:00:00', 'Обед', 1000, 100000), ('2020-01-31 20:00:00', 'Ужин', 510, 100000), ('2020-01-31 14:00:00', 'Админ ланч', 510, 100001), - ('2020-01-31 21:00:00', 'Админ ужин', 1500, 100001); + ('2020-01-31 21:00:00', 'Админ ужин', 1500, 100001); \ No newline at end of file diff --git a/src/main/resources/db/postgres.properties b/src/main/resources/db/postgres.properties index c56854a9b..ba40447d4 100644 --- a/src/main/resources/db/postgres.properties +++ b/src/main/resources/db/postgres.properties @@ -1,3 +1,7 @@ +#database.url=jdbc:postgresql://ec2-34-248-169-69.eu-west-1.compute.amazonaws.com:5432/d1ohm99dookbqn?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl.NonValidatingFactory +#database.username=qhazsiozndzrzc +#database.password=749f7852a65b5ec57bde033af8fde7f8b782a3ef802921acd4613b133d62559e + database.url=jdbc:postgresql://localhost:5432/topjava database.username=user database.password=password diff --git a/src/main/resources/db/tomcat.properties b/src/main/resources/db/tomcat.properties index e11f0725f..2e073681a 100644 --- a/src/main/resources/db/tomcat.properties +++ b/src/main/resources/db/tomcat.properties @@ -1,5 +1,5 @@ database.init=false -jdbc.initLocation=classpath:db/initDB.sql +jdbc.initLocation=initDB.sql jpa.showSql=true hibernate.format_sql=true hibernate.use_sql_comments=true \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 1d27e3f1e..ab4cfe51e 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -21,10 +21,13 @@ - + + + + - \ No newline at end of file + diff --git a/src/main/resources/spring/spring-app.xml b/src/main/resources/spring/spring-app.xml index d57b656ae..3a75ebb82 100644 --- a/src/main/resources/spring/spring-app.xml +++ b/src/main/resources/spring/spring-app.xml @@ -10,6 +10,7 @@ --> + \ No newline at end of file diff --git a/src/main/resources/spring/spring-db.xml b/src/main/resources/spring/spring-db.xml index 48afdb11a..9c090f3c5 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -93,6 +93,9 @@ + + + + + + + + @@ -23,30 +31,46 @@ + + + + + + + + + + + + - + + + + + \ 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 000000000..efcda19fe --- /dev/null +++ b/src/main/resources/spring/spring-security.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception.jsp new file mode 100644 index 000000000..90f84f4b1 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/exception.jsp @@ -0,0 +1,26 @@ +<%@ page isErrorPage="true" contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + + + + + + + + + + ${status} + + ${message} + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index 5b5efe57e..62f05f9a4 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -1,6 +1,30 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> +<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> - - | | - \ No newline at end of file + + + + + + + + + ${userTo.name} + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp index 0935c441a..cf1331fd5 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/footer.jsp @@ -1,4 +1,8 @@ <%@page contentType="text/html" pageEncoding="UTF-8" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - \ No newline at end of file +<%--https://getbootstrap.com/docs/4.0/examples/sticky-footer/--%> + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp index 0c77f1085..8c0600763 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/headTag.jsp @@ -4,7 +4,26 @@ + + + + - + + + + + + + + + + <%--http://stackoverflow.com/a/24070373/548473--%> + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp new file mode 100644 index 000000000..3e2ad09be --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/fragments/i18n.jsp @@ -0,0 +1,14 @@ +<%@ page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/index.jsp b/src/main/webapp/WEB-INF/jsp/index.jsp deleted file mode 100644 index 847191965..000000000 --- a/src/main/webapp/WEB-INF/jsp/index.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - - - - - - - : - User - Admin - - - - - - - \ 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 000000000..7f29ad7a7 --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/login.jsp @@ -0,0 +1,75 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> + + + + + + + + + + ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message} + + + + + + + » + + User + + + Admin + + + + + Spring Security, + Spring MVC, + Spring Data JPA, + Spring Security + Test, + Hibernate ORM, + Hibernate Validator, + SLF4J, + Json Jackson, + JSP, + JSTL, + Apache Tomcat, + WebJars, + DataTables, + EHCACHE, + PostgreSQL, + HSQLDB, + JUnit 5, + Hamcrest, + AssertJ, + jQuery, + jQuery plugins, + Bootstrap. + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/mealForm.jsp b/src/main/webapp/WEB-INF/jsp/mealForm.jsp deleted file mode 100644 index af6d7880e..000000000 --- a/src/main/webapp/WEB-INF/jsp/mealForm.jsp +++ /dev/null @@ -1,35 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> - - - - - - - - -<%-- `meal.new` cause javax.el.ELException - bug tomcat --%> - - - - - - : - - - - : - - - - : - - - - - - - - - diff --git a/src/main/webapp/WEB-INF/jsp/meals.jsp b/src/main/webapp/WEB-INF/jsp/meals.jsp index b42230b90..f66313bb3 100644 --- a/src/main/webapp/WEB-INF/jsp/meals.jsp +++ b/src/main/webapp/WEB-INF/jsp/meals.jsp @@ -5,60 +5,113 @@ + + - - - - - - : - - - - : - - - - : - - - - : - - - - - - - - - - - - - - - - - - - - - - <%--${meal.dateTime.toLocalDate()} ${meal.dateTime.toLocalTime()}--%> - <%--<%=TimeUtil.toString(meal.getDateTime())%>--%> - <%--${fn:replace(meal.dateTime, 'T', ' ')}--%> - ${fn:formatDateTime(meal.dateTime)} - - ${meal.description} - ${meal.calories} - - + + + + <%--https://getbootstrap.com/docs/4.0/components/card/--%> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + × + + + + + + + + "> + + + + + "> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/profile.jsp b/src/main/webapp/WEB-INF/jsp/profile.jsp new file mode 100644 index 000000000..c6d2df55f --- /dev/null +++ b/src/main/webapp/WEB-INF/jsp/profile.jsp @@ -0,0 +1,43 @@ +<%@ page contentType="text/html" pageEncoding="UTF-8" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="topjava" tagdir="/WEB-INF/tags" %> + + + + + + + + + + <%--@elvariable id="userTo" type="ru.javawebinar.topjava.to.UserTo"--%> + + + ${userTo.name} + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/jsp/users.jsp b/src/main/webapp/WEB-INF/jsp/users.jsp index 4d3d86789..498bc4943 100644 --- a/src/main/webapp/WEB-INF/jsp/users.jsp +++ b/src/main/webapp/WEB-INF/jsp/users.jsp @@ -6,33 +6,79 @@ + + - - - - - - - - - - - - - - - + + + + + + + + + - - ${user.email} - ${user.roles} - ${user.enabled} - + + + + + + + - - - + + + + + + + + + + + × + + + + + + + + "> + + + + + "> + + + + + "> + + + + + + + + + + \ 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 000000000..bc481be2d --- /dev/null +++ b/src/main/webapp/WEB-INF/tags/inputField.tag @@ -0,0 +1,15 @@ +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +<%@ attribute name="name" required="true" description="Name of corresponding property in bean object" %> +<%@ attribute name="labelCode" required="true" description="Field label" %> +<%@ attribute name="inputType" required="false" description="Input type" %> + + + + + + ${status.errorMessage} + + \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 65af7c831..9816e2a1b 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -4,7 +4,7 @@ http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> - TopJava + Topjava spring.profiles.default @@ -37,6 +37,16 @@ / + + + springSecurityFilterChain + org.springframework.web.filter.DelegatingFilterProxy + + + springSecurityFilterChain + /* + + encodingFilter org.springframework.web.filter.CharacterEncodingFilter diff --git a/src/main/webapp/resources/css/style.css b/src/main/webapp/resources/css/style.css index a55147510..9e8db29aa 100644 --- a/src/main/webapp/resources/css/style.css +++ b/src/main/webapp/resources/css/style.css @@ -1,32 +1,50 @@ -dl { - background: none repeat scroll 0 0 #FAFAFA; - margin: 8px 0; - padding: 0; +tr[data-meal-excess="false"] { + color: green; } -dt { - display: inline-block; - width: 170px; +tr[data-meal-excess="true"] { + color: red; } -dd { - display: inline-block; - margin-left: 8px; - vertical-align: top; +.fa { + cursor: pointer; } -tr[data-meal-excess="false"] { - color: green; +tr[data-user-enabled="false"] { + opacity: 0.3; } -tr[data-meal-excess="true"] { - color: red; +.error, .message { + padding: 10px; + border-radius: 4px; + font-size: 16px; +} + +.error { + color: #a94442; + background-color: #f2dede; + border: 1px solid #ebccd1; } -header, footer { - background: none repeat scroll 0 0 #A6C9E2; - color: #2E6E9E; - font-size: 20px; - padding: 5px 20px; - margin: 6px 0; +.message { + color: #2f9635; + background-color: #c6fbc2; + border: 1px solid #9feba6; +} + +/*https://getbootstrap.com/docs/4.0/examples/sticky-footer/sticky-footer.css*/ +html { + position: relative; + min-height: 100%; +} +body { + margin-bottom: 60px !important; /* Margin bottom by footer height */ +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + height: 60px; /* Set the fixed height of the footer here */ + line-height: 60px; /* Vertically center the text there */ + background-color: #f5f5f5; } diff --git a/src/main/webapp/resources/images/icon-meal.png b/src/main/webapp/resources/images/icon-meal.png new file mode 100644 index 000000000..b4fc54ad0 Binary files /dev/null and b/src/main/webapp/resources/images/icon-meal.png differ diff --git a/src/main/webapp/resources/js/topjava.common.js b/src/main/webapp/resources/js/topjava.common.js new file mode 100644 index 000000000..261659f9f --- /dev/null +++ b/src/main/webapp/resources/js/topjava.common.js @@ -0,0 +1,118 @@ +let form; + +function makeEditable(datatableOpts) { + ctx.datatableApi = $("#datatable").DataTable( + // https://api.jquery.com/jquery.extend/#jQuery-extend-deep-target-object1-objectN + $.extend(true, datatableOpts, + { + "ajax": { + "url": ctx.ajaxUrl, + "dataSrc": "" + }, + "paging": false, + "info": true + } + )); + form = $('#detailsForm'); + + $(document).ajaxError(function (event, jqXHR, options, jsExc) { + failNoty(jqXHR); + }); + + // solve problem with cache in IE: https://stackoverflow.com/a/4303862/548473 + $.ajaxSetup({cache: false}); + + var token = $("meta[name='_csrf']").attr("content"); + var header = $("meta[name='_csrf_header']").attr("content"); + $(document).ajaxSend(function (e, xhr, options) { + xhr.setRequestHeader(header, token); + }); +} + +function add() { + $("#modalTitle").html(i18n["addTitle"]); + form.find(":input").val(""); + $("#editRow").modal(); +} + +function updateRow(id) { + form.find(":input").val(""); + $("#modalTitle").html(i18n["editTitle"]); + $.get(ctx.ajaxUrl + id, function (data) { + $.each(data, function (key, value) { + form.find("input[name='" + key + "']").val(value); + }); + $('#editRow').modal(); + }); +} + +function deleteRow(id) { + if (confirm(i18n['common.confirm'])) { + $.ajax({ + url: ctx.ajaxUrl + id, + type: "DELETE" + }).done(function () { + ctx.updateTable(); + successNoty("common.deleted"); + }); + } +} + +function updateTableByData(data) { + ctx.datatableApi.clear().rows.add(data).draw(); +} + +function save() { + $.ajax({ + type: "POST", + url: ctx.ajaxUrl, + data: form.serialize() + }).done(function () { + $("#editRow").modal("hide"); + ctx.updateTable(); + successNoty("common.saved"); + }); +} + +let failedNote; + +function closeNoty() { + if (failedNote) { + failedNote.close(); + failedNote = undefined; + } +} + +function successNoty(key) { + closeNoty(); + new Noty({ + text: " " + i18n[key], + type: 'success', + layout: "bottomRight", + timeout: 1000 + }).show(); +} + +function renderEditBtn(data, type, row) { + if (type === "display") { + return ""; + } +} + +function renderDeleteBtn(data, type, row) { + if (type === "display") { + return ""; + } +} + +function failNoty(jqXHR) { + closeNoty(); + var errorInfo = jqXHR.responseJSON; + failedNote = new Noty({ + text: " " + i18n["common.errorStatus"] + ": " + jqXHR.status + + "" + errorInfo.type + "" + errorInfo.detail, + type: "error", + layout: "bottomRight" + }); + failedNote.show() +} \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.meals.js b/src/main/webapp/resources/js/topjava.meals.js new file mode 100644 index 000000000..0b3e8e7e8 --- /dev/null +++ b/src/main/webapp/resources/js/topjava.meals.js @@ -0,0 +1,119 @@ +const mealAjaxUrl = "profile/meals/"; + +// https://stackoverflow.com/a/5064235/548473 +const ctx = { + ajaxUrl: mealAjaxUrl, + updateTable: function () { + $.ajax({ + type: "GET", + url: mealAjaxUrl + "filter", + data: $("#filter").serialize() + }).done(updateTableByData); + } +} + +function clearFilter() { + $("#filter")[0].reset(); + $.get(mealAjaxUrl, updateTableByData); +} + +// http://api.jquery.com/jQuery.ajax/#using-converters +$.ajaxSetup({ + converters: { + "text json": function (stringData) { + var json = JSON.parse(stringData); + if (typeof json === 'object') { + $(json).each(function () { + if (this.hasOwnProperty('dateTime')) { + this.dateTime = this.dateTime.substr(0, 16).replace('T', ' '); + } + }); + } + return json; + } + } +}); + +$(function () { + makeEditable({ + "columns": [ + { + "data": "dateTime" + }, + { + "data": "description" + }, + { + "data": "calories" + }, + { + "render": renderEditBtn, + "defaultContent": "", + "orderable": false + }, + { + "render": renderDeleteBtn, + "defaultContent": "", + "orderable": false + } + ], + "order": [ + [ + 0, + "desc" + ] + ], + "createdRow": function (row, data, dataIndex) { + $(row).attr("data-meal-excess", data.excess); + } + }); + +// http://xdsoft.net/jqplugins/datetimepicker/ + var startDate = $('#startDate'); + var endDate = $('#endDate'); + startDate.datetimepicker({ + timepicker: false, + format: 'Y-m-d', + formatDate: 'Y-m-d', + onShow: function (ct) { + this.setOptions({ + maxDate: endDate.val() ? endDate.val() : false + }) + } + }); + endDate.datetimepicker({ + timepicker: false, + format: 'Y-m-d', + formatDate: 'Y-m-d', + onShow: function (ct) { + this.setOptions({ + minDate: startDate.val() ? startDate.val() : false + }) + } + }); + + var startTime = $('#startTime'); + var endTime = $('#endTime'); + startTime.datetimepicker({ + datepicker: false, + format: 'H:i', + onShow: function (ct) { + this.setOptions({ + maxTime: endTime.val() ? endTime.val() : false + }) + } + }); + endTime.datetimepicker({ + datepicker: false, + format: 'H:i', + onShow: function (ct) { + this.setOptions({ + minTime: startTime.val() ? startTime.val() : false + }) + } + }); + + $('#dateTime').datetimepicker({ + format: 'Y-m-d H:i' + }); +}); \ No newline at end of file diff --git a/src/main/webapp/resources/js/topjava.users.js b/src/main/webapp/resources/js/topjava.users.js new file mode 100644 index 000000000..bb8863a8f --- /dev/null +++ b/src/main/webapp/resources/js/topjava.users.js @@ -0,0 +1,86 @@ +const userAjaxUrl = "admin/users/"; + +// https://stackoverflow.com/a/5064235/548473 +const ctx = { + ajaxUrl: userAjaxUrl, + updateTable: function () { + $.get(userAjaxUrl, updateTableByData); + } +} + +function enable(chkbox, id) { + var enabled = chkbox.is(":checked"); +// https://stackoverflow.com/a/22213543/548473 + $.ajax({ + url: userAjaxUrl + id, + type: "POST", + data: "enabled=" + enabled + }).done(function () { + chkbox.closest("tr").attr("data-user-enabled", enabled); + successNoty(enabled ? "common.enabled" : "common.disabled"); + }).fail(function () { + $(chkbox).prop("checked", !enabled); + }); +} + +// $(document).ready(function () { +$(function () { + makeEditable({ + "columns": [ + { + "data": "name" + }, + { + "data": "email", + "render": function (data, type, row) { + if (type === "display") { + return "" + data + ""; + } + return data; + } + }, + { + "data": "roles" + }, + { + "data": "enabled", + "render": function (data, type, row) { + if (type === "display") { + return ""; + } + return data; + } + }, + { + "data": "registered", + "render": function (date, type, row) { + if (type === "display") { + return date.substring(0, 10); + } + return date; + } + }, + { + "orderable": false, + "defaultContent": "", + "render": renderEditBtn + }, + { + "orderable": false, + "defaultContent": "", + "render": renderDeleteBtn + } + ], + "order": [ + [ + 0, + "asc" + ] + ], + "createdRow": function (row, data, dataIndex) { + if (!data.enabled) { + $(row).attr("data-user-enabled", false); + } + } + }); +}); \ 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 000000000..e50b33277 --- /dev/null +++ b/src/main/webapp/test.html @@ -0,0 +1,13 @@ + + + + + + + + User list + Meal list + + + + \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java index 43f143cc7..da3bec700 100644 --- a/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java +++ b/src/test/java/ru/javawebinar/topjava/ActiveDbProfileResolver.java @@ -3,17 +3,17 @@ import org.springframework.lang.NonNull; import org.springframework.test.context.support.DefaultActiveProfilesResolver; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; //http://stackoverflow.com/questions/23871255/spring-profiles-simple-example-of-activeprofilesresolver public class ActiveDbProfileResolver extends DefaultActiveProfilesResolver { @Override public @NonNull String[] resolve(@NonNull Class> aClass) { - // https://stackoverflow.com/a/52438829/548473 - String[] activeProfiles = super.resolve(aClass); - String[] activeProfilesWithDb = Arrays.copyOf(activeProfiles, activeProfiles.length + 1); - activeProfilesWithDb[activeProfiles.length] = Profiles.getActiveDbProfile(); - return activeProfilesWithDb; + List profiles = new ArrayList<>(Arrays.asList(super.resolve(aClass))); + profiles.add(Profiles.getActiveDbProfile()); + return profiles.toArray(String[]::new); } } diff --git a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java index 40f3d8c22..15e01e155 100644 --- a/src/test/java/ru/javawebinar/topjava/MatcherFactory.java +++ b/src/test/java/ru/javawebinar/topjava/MatcherFactory.java @@ -7,6 +7,7 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import java.util.function.BiConsumer; import static org.assertj.core.api.Assertions.assertThat; @@ -17,21 +18,36 @@ * Support converting json MvcResult to objects for comparation. */ public class MatcherFactory { + + public static Matcher usingAssertions(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) { + return new Matcher<>(clazz, assertion, iterableAssertion); + } + + public static Matcher usingEqualsComparator(Class clazz) { + return usingAssertions(clazz, + (a, e) -> assertThat(a).isEqualTo(e), + (a, e) -> assertThat(a).isEqualTo(e)); + } + public static Matcher usingIgnoringFieldsComparator(Class clazz, String... fieldsToIgnore) { - return new Matcher<>(clazz, fieldsToIgnore); + return usingAssertions(clazz, + (a, e) -> assertThat(a).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(e), + (a, e) -> assertThat(a).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(e)); } public static class Matcher { private final Class clazz; - private final String[] fieldsToIgnore; + private final BiConsumer assertion; + private final BiConsumer, Iterable> iterableAssertion; - private Matcher(Class clazz, String... fieldsToIgnore) { + private Matcher(Class clazz, BiConsumer assertion, BiConsumer, Iterable> iterableAssertion) { this.clazz = clazz; - this.fieldsToIgnore = fieldsToIgnore; + this.assertion = assertion; + this.iterableAssertion = iterableAssertion; } public void assertMatch(T actual, T expected) { - assertThat(actual).usingRecursiveComparison().ignoringFields(fieldsToIgnore).isEqualTo(expected); + assertion.accept(actual, expected); } @SafeVarargs @@ -40,7 +56,7 @@ public final void assertMatch(Iterable actual, T... expected) { } public void assertMatch(Iterable actual, Iterable expected) { - assertThat(actual).usingRecursiveFieldByFieldElementComparatorIgnoringFields(fieldsToIgnore).isEqualTo(expected); + iterableAssertion.accept(actual, expected); } public ResultMatcher contentJson(T expected) { diff --git a/src/test/java/ru/javawebinar/topjava/MealTestData.java b/src/test/java/ru/javawebinar/topjava/MealTestData.java index c2697db04..6ee8b66dd 100644 --- a/src/test/java/ru/javawebinar/topjava/MealTestData.java +++ b/src/test/java/ru/javawebinar/topjava/MealTestData.java @@ -1,6 +1,7 @@ package ru.javawebinar.topjava; import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.to.MealTo; import java.time.Month; import java.time.temporal.ChronoUnit; @@ -11,6 +12,7 @@ public class MealTestData { public static final MatcherFactory.Matcher MEAL_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(Meal.class, "user"); + public static MatcherFactory.Matcher TO_MATCHER = MatcherFactory.usingEqualsComparator(MealTo.class); public static final int NOT_FOUND = 10; public static final int MEAL1_ID = START_SEQ + 3; diff --git a/src/test/java/ru/javawebinar/topjava/SpringMain.java b/src/test/java/ru/javawebinar/topjava/SpringMain.java index 1dda999c5..1aa3df136 100644 --- a/src/test/java/ru/javawebinar/topjava/SpringMain.java +++ b/src/test/java/ru/javawebinar/topjava/SpringMain.java @@ -1,7 +1,6 @@ package ru.javawebinar.topjava; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.MealTo; @@ -14,15 +13,24 @@ import java.util.Arrays; import java.util.List; +import static ru.javawebinar.topjava.TestUtil.mockAuthorize; +import static ru.javawebinar.topjava.UserTestData.user; + public class SpringMain { public static void main(String[] args) { // java 7 automatic resource management (ARM) - try (ConfigurableApplicationContext appCtx = new ClassPathXmlApplicationContext("spring/inmemory.xml")) { + try (GenericXmlApplicationContext appCtx = new GenericXmlApplicationContext()) { + appCtx.getEnvironment().setActiveProfiles(Profiles.getActiveDbProfile(), Profiles.REPOSITORY_IMPLEMENTATION); + appCtx.load("spring/inmemory.xml"); + appCtx.refresh(); + System.out.println("Bean definition names: " + Arrays.toString(appCtx.getBeanDefinitionNames())); AdminRestController adminUserController = appCtx.getBean(AdminRestController.class); - adminUserController.create(new User(null, "userName", "email@mail.ru", "password", Role.ADMIN)); + adminUserController.create(new User(null, "userName", "email@mail.ru", "password", 2000, Role.ADMIN)); System.out.println(); + mockAuthorize(user); + MealRestController mealController = appCtx.getBean(MealRestController.class); List filteredMealsWithExcess = mealController.getBetween( 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 000000000..3688cd60e --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/TestUtil.java @@ -0,0 +1,23 @@ +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.request.RequestPostProcessor; +import ru.javawebinar.topjava.model.User; + +public class TestUtil { + + public static void mockAuthorize(User user) { + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(new AuthorizedUser(user), null, user.getRoles())); + } + + public static RequestPostProcessor userHttpBasic(User user) { + return SecurityMockMvcRequestPostProcessors.httpBasic(user.getEmail(), user.getPassword()); + } + + public static RequestPostProcessor userAuth(User user) { + return SecurityMockMvcRequestPostProcessors.authentication(new UsernamePasswordAuthenticationToken(user.getEmail(), user.getPassword())); + } +} diff --git a/src/test/java/ru/javawebinar/topjava/UserTestData.java b/src/test/java/ru/javawebinar/topjava/UserTestData.java index 285964552..e93e9e84b 100644 --- a/src/test/java/ru/javawebinar/topjava/UserTestData.java +++ b/src/test/java/ru/javawebinar/topjava/UserTestData.java @@ -2,23 +2,39 @@ import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; +import ru.javawebinar.topjava.web.json.JsonUtil; import java.util.Collections; import java.util.Date; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; +import static ru.javawebinar.topjava.MealTestData.*; import static ru.javawebinar.topjava.model.AbstractBaseEntity.START_SEQ; public class UserTestData { - public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(User.class, "registered", "meals"); + public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(User.class, "registered", "meals", "password"); + public static MatcherFactory.Matcher USER_WITH_MEALS_MATCHER = + MatcherFactory.usingAssertions(User.class, +// No need use ignoringAllOverriddenEquals, see https://assertj.github.io/doc/#breaking-changes + (a, e) -> assertThat(a).usingRecursiveComparison().ignoringFields("registered", "meals.user", "password").isEqualTo(e), + (a, e) -> { + throw new UnsupportedOperationException(); + }); public static final int USER_ID = START_SEQ; public static final int ADMIN_ID = START_SEQ + 1; public static final int GUEST_ID = START_SEQ + 2; public static final int NOT_FOUND = 10; - public static final User user = new User(USER_ID, "User", "user@yandex.ru", "password", Role.USER); - public static final User admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", Role.ADMIN, Role.USER); - public static final User guest = new User(GUEST_ID, "Guest", "guest@gmail.com", "guest"); + public static final User user = new User(USER_ID, "User", "user@yandex.ru", "password", 2005, Role.USER); + public static final User admin = new User(ADMIN_ID, "Admin", "admin@gmail.com", "admin", 1900, Role.ADMIN, Role.USER); + public static final User guest = new User(GUEST_ID, "Guest", "guest@gmail.com", "guest", 2000); + + static { + user.setMeals(meals); + admin.setMeals(List.of(adminMeal2, adminMeal1)); + } public static User getNew() { return new User(null, "New", "new@gmail.com", "newPass", 1555, false, new Date(), Collections.singleton(Role.USER)); @@ -34,4 +50,8 @@ public static User getUpdated() { updated.setRoles(Collections.singletonList(Role.ADMIN)); return updated; } + + public static String jsonWithPassword(User user, String passw) { + return JsonUtil.writeAdditionProps(user, "password", passw); + } } diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java index 930548690..fb1626c4c 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractServiceTest.java @@ -8,8 +8,8 @@ import ru.javawebinar.topjava.ActiveDbProfileResolver; import ru.javawebinar.topjava.TimingExtension; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static ru.javawebinar.topjava.util.ValidationUtil.getRootCause; @SpringJUnitConfig(locations = { "classpath:spring/spring-app.xml", @@ -17,17 +17,18 @@ }) //@ExtendWith(SpringExtension.class) @ActiveProfiles(resolver = ActiveDbProfileResolver.class) -@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8")) +@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8"), executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) @ExtendWith(TimingExtension.class) public abstract class AbstractServiceTest { - // Check root cause with AssertJ: https://github.com/junit-team/junit-framework/issues/2129#issuecomment-565712630 // Check root cause in JUnit: https://github.com/junit-team/junit4/pull/778 protected void validateRootCause(Class rootExceptionClass, Runnable runnable) { - assertThatExceptionOfType(Throwable.class) - .isThrownBy(runnable::run) - .satisfiesAnyOf( - ex -> assertThat(ex).isInstanceOf(rootExceptionClass), - ex -> assertThat(ex).hasRootCauseInstanceOf(rootExceptionClass)); + assertThrows(rootExceptionClass, () -> { + try { + runnable.run(); + } catch (Exception e) { + throw getRootCause(e); + } + }); } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java index fe6838b5f..2566c1c76 100644 --- a/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/AbstractUserServiceTest.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.Set; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import static ru.javawebinar.topjava.UserTestData.*; public abstract class AbstractUserServiceTest extends AbstractServiceTest { @@ -21,7 +21,7 @@ public abstract class AbstractUserServiceTest extends AbstractServiceTest { protected UserService service; @Test - void create() { + public void create() { User created = service.create(getNew()); int newId = created.id(); User newUser = getNew(); @@ -33,7 +33,7 @@ void create() { @Test void duplicateMailCreate() { assertThrows(DataAccessException.class, () -> - service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", Role.USER))); + service.create(new User(null, "Duplicate", "user@yandex.ru", "newPass", 2000, Role.USER))); } @Test @@ -79,10 +79,17 @@ void getAll() { @Test void createWithException() throws Exception { - validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, " ", "mail@yandex.ru", "password", Role.USER))); - validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", Role.USER))); - validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", " ", Role.USER))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, " ", "mail@yandex.ru", "password", 2000, Role.USER))); + validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", " ", "password", 2000, Role.USER))); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", "password", 9, true, new Date(), Set.of()))); validateRootCause(ConstraintViolationException.class, () -> service.create(new User(null, "User", "mail@yandex.ru", "password", 10001, true, new Date(), Set.of()))); } + + @Test + void enable() { + service.enable(USER_ID, false); + assertFalse(service.get(USER_ID).isEnabled()); + service.enable(USER_ID, true); + assertTrue(service.get(USER_ID).isEnabled()); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java index d8a1f4106..3638e07e9 100644 --- a/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java +++ b/src/test/java/ru/javawebinar/topjava/service/datajpa/DataJpaUserServiceTest.java @@ -3,13 +3,10 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; -import ru.javawebinar.topjava.MealTestData; -import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.AbstractUserServiceTest; import ru.javawebinar.topjava.util.exception.NotFoundException; -import static ru.javawebinar.topjava.MealTestData.MEAL_MATCHER; import static ru.javawebinar.topjava.Profiles.DATAJPA; import static ru.javawebinar.topjava.UserTestData.*; @@ -17,9 +14,8 @@ class DataJpaUserServiceTest extends AbstractUserServiceTest { @Test void getWithMeals() { - User admin = service.getWithMeals(ADMIN_ID); - USER_MATCHER.assertMatch(admin, UserTestData.admin); - MEAL_MATCHER.assertMatch(admin.getMeals(), MealTestData.adminMeal2, MealTestData.adminMeal1); + User actual = service.getWithMeals(ADMIN_ID); + USER_WITH_MEALS_MATCHER.assertMatch(actual, admin); } @Test diff --git a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java index 6fa9b1d4c..c12f7c53e 100644 --- a/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/AbstractControllerTest.java @@ -1,6 +1,8 @@ package ru.javawebinar.topjava.web; +import org.junit.jupiter.api.Assumptions; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.web.servlet.MockMvc; @@ -15,6 +17,9 @@ import javax.annotation.PostConstruct; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + + @SpringJUnitWebConfig(locations = { "classpath:spring/spring-app.xml", "classpath:spring/spring-mvc.xml", @@ -28,6 +33,9 @@ public abstract class AbstractControllerTest { private static final CharacterEncodingFilter CHARACTER_ENCODING_FILTER = new CharacterEncodingFilter(); + @Autowired + public Environment env; + static { CHARACTER_ENCODING_FILTER.setEncoding("UTF-8"); CHARACTER_ENCODING_FILTER.setForceEncoding(true); @@ -38,11 +46,16 @@ public abstract class AbstractControllerTest { @Autowired private WebApplicationContext webApplicationContext; + public void assumeDataJpa() { + Assumptions.assumeTrue(env.acceptsProfiles(org.springframework.core.env.Profiles.of(Profiles.DATAJPA)), "DATA-JPA only"); + } + @PostConstruct private void postConstruct() { mockMvc = MockMvcBuilders .webAppContextSetup(webApplicationContext) .addFilter(CHARACTER_ENCODING_FILTER) + .apply(springSecurity()) .build(); } 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 000000000..244399662 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/ResourceControllerTest.java @@ -0,0 +1,20 @@ +package ru.javawebinar.topjava.web; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class ResourceControllerTest extends AbstractControllerTest { + + @Test + void resources() throws Exception { + perform(get("/resources/css/style.css")) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(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 index 2bec37dda..c200fb4e9 100644 --- a/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/RootControllerTest.java @@ -1,32 +1,41 @@ package ru.javawebinar.topjava.web; -import org.assertj.core.matcher.AssertionMatcher; import org.junit.jupiter.api.Test; -import ru.javawebinar.topjava.model.User; - -import java.util.List; 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.UserTestData.*; +import static ru.javawebinar.topjava.TestUtil.userAuth; +import static ru.javawebinar.topjava.UserTestData.admin; +import static ru.javawebinar.topjava.UserTestData.user; class RootControllerTest extends AbstractControllerTest { @Test void getUsers() throws Exception { - perform(get("/users")) + perform(get("/users") + .with(userAuth(admin))) .andDo(print()) .andExpect(status().isOk()) .andExpect(view().name("users")) - .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")) - .andExpect(model().attribute("users", - new AssertionMatcher>() { - @Override - public void assertion(List actual) throws AssertionError { - USER_MATCHER.assertMatch(actual, admin, guest, user); - } - } - )); + .andExpect(forwardedUrl("/WEB-INF/jsp/users.jsp")); + } + + @Test + void unAuth() throws Exception { + perform(get("/users")) + .andDo(print()) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @Test + void getMeals() throws Exception { + perform(get("/meals") + .with(userAuth(user))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(view().name("meals")) + .andExpect(forwardedUrl("/WEB-INF/jsp/meals.jsp")); } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java index 540586d11..d1a7a5d6c 100644 --- a/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/json/JsonUtilTest.java @@ -4,10 +4,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.model.User; import java.util.List; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.UserTestData.jsonWithPassword; +import static ru.javawebinar.topjava.UserTestData.user; class JsonUtilTest { private static final Logger log = LoggerFactory.getLogger(JsonUtilTest.class); @@ -27,4 +34,15 @@ void readWriteValues() { List actual = JsonUtil.readValues(json, Meal.class); MEAL_MATCHER.assertMatch(actual, meals); } + + @Test + void writeOnlyAccess() { + String json = JsonUtil.writeValue(user); + System.out.println(json); + assertThat(json, not(containsString("password"))); + String jsonWithPass = jsonWithPassword(user, "newPass"); + System.out.println(jsonWithPass); + User user = JsonUtil.readValue(jsonWithPass, User.class); + assertEquals(user.getPassword(), "newPass"); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java new file mode 100644 index 000000000..784bc0479 --- /dev/null +++ b/src/test/java/ru/javawebinar/topjava/web/meal/MealRestControllerTest.java @@ -0,0 +1,127 @@ +package ru.javawebinar.topjava.web.meal; + + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.model.Meal; +import ru.javawebinar.topjava.service.MealService; +import ru.javawebinar.topjava.util.exception.NotFoundException; +import ru.javawebinar.topjava.web.AbstractControllerTest; +import ru.javawebinar.topjava.web.json.JsonUtil; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.MealTestData.*; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; +import static ru.javawebinar.topjava.UserTestData.USER_ID; +import static ru.javawebinar.topjava.UserTestData.user; +import static ru.javawebinar.topjava.util.MealsUtil.createTo; +import static ru.javawebinar.topjava.util.MealsUtil.getTos; + +class MealRestControllerTest extends AbstractControllerTest { + + private static final String REST_URL = MealRestController.REST_URL + '/'; + + @Autowired + private MealService mealService; + + @Test + void get() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + MEAL1_ID) + .with(userHttpBasic(user))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(MEAL_MATCHER.contentJson(meal1)); + } + + @Test + void getUnauth() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + MEAL1_ID)) + .andExpect(status().isUnauthorized()); + } + + @Test + void getNotFound() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(user))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void delete() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + MEAL1_ID) + .with(userHttpBasic(user))) + .andExpect(status().isNoContent()); + assertThrows(NotFoundException.class, () -> mealService.get(MEAL1_ID, USER_ID)); + } + + @Test + void deleteNotFound() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + ADMIN_MEAL_ID) + .with(userHttpBasic(user))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void update() throws Exception { + Meal updated = getUpdated(); + perform(MockMvcRequestBuilders.put(REST_URL + MEAL1_ID).contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(user)) + .content(JsonUtil.writeValue(updated))) + .andExpect(status().isNoContent()); + + MEAL_MATCHER.assertMatch(mealService.get(MEAL1_ID, USER_ID), updated); + } + + @Test + void createWithLocation() throws Exception { + Meal newMeal = getNew(); + ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(user)) + .content(JsonUtil.writeValue(newMeal))) + .andExpect(status().isCreated()); + + Meal created = MEAL_MATCHER.readFromJson(action); + int newId = created.id(); + newMeal.setId(newId); + MEAL_MATCHER.assertMatch(created, newMeal); + MEAL_MATCHER.assertMatch(mealService.get(newId, USER_ID), newMeal); + } + + @Test + void getAll() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL) + .with(userHttpBasic(user))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(TO_MATCHER.contentJson(getTos(meals, user.getCaloriesPerDay()))); + } + + @Test + void getBetween() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + "filter") + .param("startDate", "2020-01-30").param("startTime", "07:00") + .param("endDate", "2020-01-31").param("endTime", "11:00") + .with(userHttpBasic(user))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(TO_MATCHER.contentJson(createTo(meal5, true), createTo(meal1, false))); + } + + @Test + void getBetweenAll() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + "filter?startDate=&endTime=") + .with(userHttpBasic(user))) + .andExpect(status().isOk()) + .andExpect(TO_MATCHER.contentJson(getTos(meals, user.getCaloriesPerDay()))); + } +} \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java index 6af80902c..dc52b0083 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/AdminRestControllerTest.java @@ -5,16 +5,18 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import ru.javawebinar.topjava.UserTestData; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; import ru.javawebinar.topjava.util.exception.NotFoundException; import ru.javawebinar.topjava.web.AbstractControllerTest; -import ru.javawebinar.topjava.web.json.JsonUtil; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; class AdminRestControllerTest extends AbstractControllerTest { @@ -26,7 +28,8 @@ class AdminRestControllerTest extends AbstractControllerTest { @Test void get() throws Exception { - perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID)) + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID) + .with(userHttpBasic(admin))) .andExpect(status().isOk()) .andDo(print()) // https://jira.spring.io/browse/SPR-14472 @@ -34,9 +37,18 @@ void get() throws Exception { .andExpect(USER_MATCHER.contentJson(admin)); } + @Test + void getNotFound() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL + NOT_FOUND) + .with(userHttpBasic(admin))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + @Test void getByEmail() throws Exception { - perform(MockMvcRequestBuilders.get(REST_URL + "by-email?email=" + user.getEmail())) + perform(MockMvcRequestBuilders.get(REST_URL + "by-email?email=" + user.getEmail()) + .with(userHttpBasic(admin))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(USER_MATCHER.contentJson(user)); @@ -44,18 +56,41 @@ void getByEmail() throws Exception { @Test void delete() throws Exception { - perform(MockMvcRequestBuilders.delete(REST_URL + USER_ID)) + perform(MockMvcRequestBuilders.delete(REST_URL + USER_ID) + .with(userHttpBasic(admin))) .andDo(print()) .andExpect(status().isNoContent()); assertThrows(NotFoundException.class, () -> userService.get(USER_ID)); } + @Test + void deleteNotFound() throws Exception { + perform(MockMvcRequestBuilders.delete(REST_URL + NOT_FOUND) + .with(userHttpBasic(admin))) + .andDo(print()) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + void getUnAuth() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + + @Test + void getForbidden() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL) + .with(userHttpBasic(user))) + .andExpect(status().isForbidden()); + } + @Test void update() throws Exception { User updated = getUpdated(); perform(MockMvcRequestBuilders.put(REST_URL + USER_ID) .contentType(MediaType.APPLICATION_JSON) - .content(JsonUtil.writeValue(updated))) + .with(userHttpBasic(admin)) + .content(jsonWithPassword(updated, updated.getPassword()))) .andExpect(status().isNoContent()); USER_MATCHER.assertMatch(userService.get(USER_ID), updated); @@ -66,7 +101,8 @@ void createWithLocation() throws Exception { User newUser = getNew(); ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) .contentType(MediaType.APPLICATION_JSON) - .content(JsonUtil.writeValue(newUser))) + .with(userHttpBasic(admin)) + .content(jsonWithPassword(newUser, newUser.getPassword()))) .andExpect(status().isCreated()); User created = USER_MATCHER.readFromJson(action); @@ -78,9 +114,33 @@ void createWithLocation() throws Exception { @Test void getAll() throws Exception { - perform(MockMvcRequestBuilders.get(REST_URL)) + perform(MockMvcRequestBuilders.get(REST_URL) + .with(userHttpBasic(admin))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(USER_MATCHER.contentJson(admin, guest, user)); } + + @Test + void getWithMeals() throws Exception { + assumeDataJpa(); + perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID + "/with-meals") + .with(userHttpBasic(admin))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_WITH_MEALS_MATCHER.contentJson(admin)); + } + + @Test + void enable() throws Exception { + perform(MockMvcRequestBuilders.patch(REST_URL + USER_ID) + .param("enabled", "false") + .contentType(MediaType.APPLICATION_JSON) + .with(userHttpBasic(admin))) + .andDo(print()) + .andExpect(status().isNoContent()); + + assertFalse(userService.get(USER_ID).isEnabled()); + } } \ No newline at end of file diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java index 7568d0f52..40af802e0 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerSpringTest.java @@ -21,7 +21,7 @@ class InMemoryAdminRestControllerSpringTest { private InMemoryUserRepository repository; @BeforeEach - void setup() { + public void setUp() { repository.init(); } diff --git a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java index c41fa0e6b..75b8a5a82 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/InMemoryAdminRestControllerTest.java @@ -30,13 +30,13 @@ static void beforeClass() { @AfterAll static void afterClass() { - // May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed - // http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite - // appCtx.close(); +// May cause during JUnit "Cache is not alive (STATUS_SHUTDOWN)" as JUnit share Spring context for speed +// http://stackoverflow.com/questions/16281802/ehcache-shutdown-causing-an-exception-while-running-test-suite +// appCtx.close(); } @BeforeEach - void setup() { + public void setup() { // re-initialize repository.init(); } diff --git a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java index e8882742d..ae91e2da9 100644 --- a/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java +++ b/src/test/java/ru/javawebinar/topjava/web/user/ProfileRestControllerTest.java @@ -3,15 +3,19 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; +import ru.javawebinar.topjava.to.UserTo; +import ru.javawebinar.topjava.util.UserUtil; import ru.javawebinar.topjava.web.AbstractControllerTest; import ru.javawebinar.topjava.web.json.JsonUtil; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static ru.javawebinar.topjava.TestUtil.userHttpBasic; import static ru.javawebinar.topjava.UserTestData.*; import static ru.javawebinar.topjava.web.user.ProfileRestController.REST_URL; @@ -22,27 +26,64 @@ class ProfileRestControllerTest extends AbstractControllerTest { @Test void get() throws Exception { - perform(MockMvcRequestBuilders.get(REST_URL)) + perform(MockMvcRequestBuilders.get(REST_URL) + .with(userHttpBasic(user))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(USER_MATCHER.contentJson(user)); } + @Test + void getUnAuth() throws Exception { + perform(MockMvcRequestBuilders.get(REST_URL)) + .andExpect(status().isUnauthorized()); + } + @Test void delete() throws Exception { - perform(MockMvcRequestBuilders.delete(REST_URL)) + perform(MockMvcRequestBuilders.delete(REST_URL) + .with(userHttpBasic(user))) .andExpect(status().isNoContent()); USER_MATCHER.assertMatch(userService.getAll(), admin, guest); } + @Test + void register() throws Exception { + UserTo newTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword", 1500); + User newUser = UserUtil.createNewFromTo(newTo); + ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL) + .contentType(MediaType.APPLICATION_JSON) + .content(JsonUtil.writeValue(newTo))) + .andDo(print()) + .andExpect(status().isCreated()); + + User created = USER_MATCHER.readFromJson(action); + int newId = created.id(); + newUser.setId(newId); + USER_MATCHER.assertMatch(created, newUser); + USER_MATCHER.assertMatch(userService.get(newId), newUser); + } + @Test void update() throws Exception { - User updated = getUpdated(); + UserTo updatedTo = new UserTo(null, "newName", "user@yandex.ru", "newPassword", 1500); perform(MockMvcRequestBuilders.put(REST_URL).contentType(MediaType.APPLICATION_JSON) - .content(JsonUtil.writeValue(updated))) + .with(userHttpBasic(user)) + .content(JsonUtil.writeValue(updatedTo))) .andDo(print()) .andExpect(status().isNoContent()); - USER_MATCHER.assertMatch(userService.get(USER_ID), updated); + USER_MATCHER.assertMatch(userService.get(USER_ID), UserUtil.updateFromTo(new User(user), updatedTo)); + } + + @Test + void getWithMeals() throws Exception { + assumeDataJpa(); + perform(MockMvcRequestBuilders.get(REST_URL + "/with-meals") + .with(userHttpBasic(user))) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(USER_WITH_MEALS_MATCHER.contentJson(user)); } } \ No newline at end of file diff --git a/src/test/resources/spring/inmemory.xml b/src/test/resources/spring/inmemory.xml index 0c9d05028..f7e2dbbd4 100644 --- a/src/test/resources/spring/inmemory.xml +++ b/src/test/resources/spring/inmemory.xml @@ -6,4 +6,5 @@ + \ No newline at end of file diff --git a/src/test/resources/spring/spring-cache.xml b/src/test/resources/spring/spring-cache.xml index 7c9dfda9a..ea51df903 100644 --- a/src/test/resources/spring/spring-cache.xml +++ b/src/test/resources/spring/spring-cache.xml @@ -10,10 +10,10 @@ http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"> - --->