diff --git a/README.md b/README.md index 118845c09..7fd61b43a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/bee16f3145654047a0505c62aeefd8a2)](https://app.codacy.com/gh/JavaWebinar/topjava/dashboard) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/bee16f3145654047a0505c62aeefd8a2)](https://www.codacy.com/gh/JavaWebinar/topjava/dashboard) + Java Enterprise Online Project =============================== @@ -10,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/) -### 25.09: Старт проекта -- Начало проверки [вступительного задания HW0](https://github.com/JavaOPs/topjava#-Домашнее-задание-hw0) +### 26.05: Старт проекта +- Начало проверки [вступительного задания](https://github.com/JavaOPs/topjava#-Домашнее-задание-hw0) -#### 30.09 Дедлайн на сдачу HW0 -### 02.10: 1-е занятие +#### 31.05 Дедлайн на сдачу HW0 +### 02.06: 1-е занятие +#### 03.06 Дедлайн подачи заявки на [дипломную программу](https://javaops.ru/view/register/diploma) - Разбор домашнего задания вступительного занятия (вместе с Optional) - Обзор используемых в проекте технологий. Интеграция ПО - Maven @@ -25,7 +27,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Уровни и зависимости логгирования. JMX - Домашнее задание 1-го занятия (HW1 + Optional) -### 09.10: 2-е занятие +### 09.06: 2-е занятие - Разбор домашнего задания HW1 + Optional - Библиотека vs Фреймворк. Стандартные библиотеки Apache Commons, Guava - Слои приложения. Создание каркаса приложения @@ -33,7 +35,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Пояснения к HW2. Обработка Autowired - Домашнее задание (HW2 + Optional) -### 16.10: 3-е занятие +### 16.06: 3-е занятие - Разбор домашнего задания HW2 + Optional - Жизненный цикл Spring контекста - Тестирование через JUnit @@ -46,7 +48,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Логирование тестов - Домашнее задание (HW3 + Optional) -### 23.10: 4-е занятие +### 23.06: 4-е занятие - Разбор домашнего задания HW3 + Optional - Методы улучшения качества кода - Spring: инициализация и популирование DB @@ -56,7 +58,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Домашнее задание (HW4 + Optional) #### Начало выполнения [выпускного проекта](https://github.com/JavaOPs/topjava/blob/master/graduation.md) -### 30.10: 5-е занятие +### 30.06: 5-е занятие - Обзор JDK 9/17. Миграция Topjava с 1.8 на 17 - Разбор вопросов - Разбор домашнего задания HW4 + Optional @@ -67,7 +69,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Spring кэш - Домашнее задание (HW5 + Optional) -### 06.11: 6-е занятие +### 07.07: 6-е занятие - Разбор домашнего задания HW5 + Optional - Кэш Hibernate - Spring Web @@ -80,7 +82,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + #### Большое ДЗ + выпускной проект + начинаем [курс BootJava](https://javaops.ru/view/bootjava) + подтягиваем "хвосты". -### 20.11: 7-е занятие +### 21.07: 7-е занятие - Разбор домашнего задания HW6 + Optional - Автогенерация DDL по модели - Тестирование Spring MVC @@ -91,7 +93,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Тестирование через SoapUi. UTF-8 - Домашнее задание (HW7 + Optional) -### 27.11: 8-е занятие +### 28.07: 8-е занятие - Разбор домашнего задания HW7 + Optional - WebJars. jQuery и JavaScript frameworks - Bootstrap @@ -100,7 +102,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Добавление Spring Security - Домашнее задание (HW8 + Optional) -### 04.12: 9-е занятие +### 04.08: 9-е занятие - Разбор домашнего задания HW8 + Optional - Spring Binding - Spring Validation @@ -112,7 +114,7 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Cookie. Session - Домашнее задание (HW9 + Optional) -### 11.12: 10-е занятие +### 11.08: 10-е занятие - Разбор домашнего задания HW10 + Optional - Кастомизация JSON (@JsonView) и валидации (groups) - Рефакторинг: jQuery конверторы и группы валидации по умолчанию @@ -125,22 +127,23 @@ Maven/ Spring/ Security/ JPA(Hibernate)/ REST(Jackson)/ Bootstrap(CSS)/ jQuery + - Защита от межсайтовой подделки запросов (CSRF) - Домашнее задание (HW10) -### 18.12: 11-е занятие +### 18.08: 11-е занятие - Разбор домашнего задания HW10 + Optional - Локализация datatables, ошибок валидации - Защита от XSS (Cross Site Scripting) - Обработка ошибок 404 (NotFound) - Доступ к AuthorizedUser - Ограничение модификации пользователей -- Деплой приложения [на собственный выделенный сервер](https://github.com/JavaOPs/startup) -- Домашнее задание (HW11): сокрытия полей в Swagger -- Составление резюме. Собеседование. Разработка ПО. Возможные доработки приложения - -### 22.12: Миграция на 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 -### 12.01: Дедлайн на сдачу [выпускного проекта](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/curl.md b/config/curl.md index 76852a8a1..fbc748aa0 100644 --- a/config/curl.md +++ b/config/curl.md @@ -36,4 +36,3 @@ #### 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` -`curl -s -X PUT -d '{"dateTime":"2015-05-30T07:00"}' -H 'Content-Type: application/json' http://localhost:8080/topjava/rest/profile/meals/100003 --user user@yandex.ru:password` diff --git a/config/db.properties b/config/db.properties deleted file mode 100644 index cdec2d890..000000000 --- a/config/db.properties +++ /dev/null @@ -1,8 +0,0 @@ -database.url=jdbc:postgresql://localhost:5432/topjava -database.username=user -database.password=password -database.init=false -jdbc.initLocation=classpath:db/initDB.sql -jpa.showSql=false -hibernate.format_sql=false -hibernate.use_sql_comments=false \ No newline at end of file diff --git a/config/messages/app.properties b/config/messages/app.properties index 0afa010e2..8dec4e48f 100644 --- a/config/messages/app.properties +++ b/config/messages/app.properties @@ -2,8 +2,8 @@ app.title=Calories management app.stackTitle=Application stack: app.description=Java Enterprise project with registration/authorization and role-based access rights (USER, ADMIN). \ Admin could create/edit/delete users, users - manage your profile and data (meals) via UI (AJAX) and REST with basic authorization. \ -Meals could be filtered by date and time. Meal record color depends on daily calories sum exceeding "Daily calorie limit" (editable user's profile paramets). \ -All REST interface covered with JUnit tests by Spring MVC Test and Spring Security Test. +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 @@ -21,11 +21,6 @@ user.registered=Registered user.password=Password user.caloriesPerDay=Daily calorie limit -userTo.name=Name -userTo.email=Email -userTo.password=Password -userTo.caloriesPerDay=Daily calorie limit - meal.title=Meals meal.edit=Edit meal meal.add=Add meal @@ -44,25 +39,8 @@ 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 -common.search=Search - -exception.user.duplicateEmail=User with this email already exists -exception.user.updateRestriction=Admin/User update is forbidden -exception.meal.duplicateDateTime=You already have meal with this date/time - -error.appError=Application error -error.dataNotFound=Data not found -error.dataError=Data error -error.validationError=Validation error -error.wrongRequest=Wrong request - -NotEmpty=[{0}] must not be empty -NotBlank=[{0}] must not be empty -NotNull=[{0}] must not be empty -Email= Invalid format of [{0}] -Range=[{0}] must be between {2} and {1} -Length=[{0}] length must be between {2} and {1} -Size=[{0}] size must be between {2} and {1} \ No newline at end of file +common.cancel=Cancel \ No newline at end of file diff --git a/config/messages/app_ru.properties b/config/messages/app_ru.properties index cb8a254ea..be85ad30e 100644 --- a/config/messages/app_ru.properties +++ b/config/messages/app_ru.properties @@ -21,11 +21,6 @@ user.registered=Зарегистрирован user.password=Пароль user.caloriesPerDay=Норма калорий в день -userTo.name=Имя -userTo.email=Почта -userTo.password=Пароль -userTo.caloriesPerDay=Норма калорий в день - meal.title=Моя еда meal.edit=Редактировать еду meal.add=Добавить еду @@ -44,25 +39,8 @@ common.deleted=Запись удалена common.saved=Запись сохранена common.enabled=Запись активирована common.disabled=Запись деактивирована +common.errorStatus=Статус ошибки +common.appError=Ошибка приложения common.confirm=Вы уверены? common.save=Сохранить -common.cancel=Отменить -common.search=Искать - -exception.user.duplicateEmail=Пользователь с такой почтой уже есть в приложении -exception.user.updateRestriction=Изменение Admin/User запрещено -exception.meal.duplicateDateTime=У вас уже есть еда с такой датой/временем - -error.appError=Ошибка приложения -error.dataNotFound=Данные не найдены -error.dataError=Ошибка в данных -error.validationError=Ошибка проверки данных -error.wrongRequest=Неверный запрос - -NotEmpty=[{0}] не должно быть пустым -NotBlank=[{0}] не должно быть пустым -NotNull=[{0}] не должно быть пустым -Email=Неверный формат [{0}] -Range= [{0}] должно быть между {2} и {1} -Length=Длинна [{0}] должна быть между {2} и {1} -Size=Размер [{0}] должен быть между {2} и {1} \ No newline at end of file +common.cancel=Отменить \ No newline at end of file diff --git a/pom.xml b/pom.xml index e3ccaaae4..c32746868 100644 --- a/pom.xml +++ b/pom.xml @@ -9,70 +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 - 5.8.16 - 2.7.18 - 2.20.1 - 9.0.111 - 1.21.2 + + 5.3.20 + 2.7.1 + 5.7.2 + + 2.13.3 + 9.0.64 + + + 1.2.11 + 1.7.36 + + + 42.4.0 - 5.6.15.Final - 6.2.5.Final + 5.6.9.Final + 6.2.3.Final 3.0.1-b12 - 3.10.8 + 3.10.0 + + + 5.8.2 + 3.23.1 + 2.2 - 4.6.2 - 3.7.1 + 4.6.1 + 3.6.0 2.5.20-1 3.1.4 - 1.13.5 - - - 1.5.20 - 2.0.17 - - - 42.7.8 - - 5.14.1 - 3.27.6 - 3.0 - 2.10.0 + 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 @@ -83,7 +84,7 @@ org.codehaus.cargo cargo-maven3-plugin - 1.10.24 + 1.9.13 tomcat9x @@ -128,6 +129,7 @@ org.slf4j slf4j-api ${slf4j.version} + compile @@ -137,14 +139,6 @@ runtime - - - com.google.code.findbugs - annotations - 3.0.1 - compile - - javax.annotation javax.annotation-api @@ -162,17 +156,6 @@ ${spring-data-jpa.version} - - io.springfox - springfox-swagger2 - 2.9.2 - - - io.springfox - springfox-swagger-ui - 2.9.2 - - org.springframework.security @@ -196,17 +179,15 @@ hibernate-core ${hibernate.version} - - org.hibernate.validator hibernate-validator ${hibernate-validator.version} - org.jsoup - jsoup - ${jsoup.version} + org.hibernate + hibernate-jcache + ${hibernate.version} @@ -218,11 +199,6 @@ - - org.hibernate - hibernate-jcache - ${hibernate.version} - javax.cache cache-api @@ -283,7 +259,7 @@ jquery - + org.webjars popper.js @@ -332,7 +308,7 @@ org.junit.jupiter junit-jupiter-engine - ${junit.version} + ${junit.jupiter.version} test @@ -341,12 +317,6 @@ ${hamcrest.version} test - - com.jayway.jsonpath - json-path - ${json-path.version} - test - org.springframework @@ -371,7 +341,7 @@ org.junit.platform junit-platform-launcher - 1.14.1 + 1.8.2 test @@ -383,7 +353,7 @@ org.hsqldb hsqldb - 2.7.4 + 2.6.1 @@ -426,4 +396,4 @@ - \ No newline at end of file + diff --git a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java index b51930e87..2126d687b 100644 --- a/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java +++ b/src/main/java/ru/javawebinar/topjava/AuthorizedUser.java @@ -2,7 +2,7 @@ import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; -import ru.javawebinar.topjava.util.UsersUtil; +import ru.javawebinar.topjava.util.UserUtil; import java.io.Serial; @@ -14,7 +14,7 @@ public class AuthorizedUser extends org.springframework.security.core.userdetail public AuthorizedUser(User user) { super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles()); - setTo(UsersUtil.asTo(user)); + setTo(UserUtil.asTo(user)); } public int getId() { diff --git a/src/main/java/ru/javawebinar/topjava/HasIdAndEmail.java b/src/main/java/ru/javawebinar/topjava/HasIdAndEmail.java deleted file mode 100644 index 6389876b5..000000000 --- a/src/main/java/ru/javawebinar/topjava/HasIdAndEmail.java +++ /dev/null @@ -1,5 +0,0 @@ -package ru.javawebinar.topjava; - -public interface HasIdAndEmail extends HasId { - String getEmail(); -} \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/Profiles.java b/src/main/java/ru/javawebinar/topjava/Profiles.java index 80dc6ef11..e4111a2af 100644 --- a/src/main/java/ru/javawebinar/topjava/Profiles.java +++ b/src/main/java/ru/javawebinar/topjava/Profiles.java @@ -12,10 +12,9 @@ public class Profiles { public static final String POSTGRES_DB = "postgres", - HSQL_DB = "hsqldb", - VDS = "vds"; + HSQL_DB = "hsqldb"; - // Get DB profile depending on DB driver in classpath + // Get DB profile depending of DB driver in classpath public static String getActiveDbProfile() { if (ClassUtils.isPresent("org.postgresql.Driver", null)) { return POSTGRES_DB; diff --git a/src/main/java/ru/javawebinar/topjava/View.java b/src/main/java/ru/javawebinar/topjava/View.java index f1623c053..62c332bcf 100644 --- a/src/main/java/ru/javawebinar/topjava/View.java +++ b/src/main/java/ru/javawebinar/topjava/View.java @@ -3,9 +3,5 @@ import javax.validation.groups.Default; public class View { - // Validate only form UI/REST - public interface Web extends Default {} - - // Validate only when DB save/update public interface Persist extends Default {} } \ No newline at end of file diff --git a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java index ad5ded601..536c5c986 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractBaseEntity.java @@ -1,12 +1,11 @@ package ru.javawebinar.topjava.model; -import io.swagger.annotations.ApiModelProperty; +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) @@ -20,7 +19,6 @@ public abstract class AbstractBaseEntity implements HasId { @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq") // See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034 // Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting - @ApiModelProperty(hidden = true) protected Integer id; protected AbstractBaseEntity() { @@ -42,19 +40,23 @@ public Integer getId() { @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/AbstractNamedEntity.java b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java index 2539be1fe..0b32aac58 100644 --- a/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java +++ b/src/main/java/ru/javawebinar/topjava/model/AbstractNamedEntity.java @@ -2,18 +2,16 @@ import javax.persistence.Column; import javax.persistence.MappedSuperclass; -import ru.javawebinar.topjava.View; -import ru.javawebinar.topjava.util.validation.NoHtml; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; + @MappedSuperclass public abstract class AbstractNamedEntity extends AbstractBaseEntity { @NotBlank @Size(min = 2, max = 128) @Column(name = "name", nullable = false) - @NoHtml(groups = {View.Web.class}) protected String name; protected AbstractNamedEntity() { diff --git a/src/main/java/ru/javawebinar/topjava/model/Meal.java b/src/main/java/ru/javawebinar/topjava/model/Meal.java index 50a2c1d6a..ed5063121 100644 --- a/src/main/java/ru/javawebinar/topjava/model/Meal.java +++ b/src/main/java/ru/javawebinar/topjava/model/Meal.java @@ -1,13 +1,12 @@ package ru.javawebinar.topjava.model; -import com.fasterxml.jackson.annotation.JsonIgnore; +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 ru.javawebinar.topjava.util.validation.NoHtml; import javax.persistence.*; import javax.validation.constraints.NotBlank; @@ -28,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"; @@ -42,7 +41,6 @@ public class Meal extends AbstractBaseEntity { @Column(name = "description", nullable = false) @NotBlank @Size(min = 2, max = 120) - @NoHtml(groups = {View.Web.class}) private String description; @Column(name = "calories", nullable = false) @@ -53,7 +51,7 @@ public class Meal extends AbstractBaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) - @JsonIgnore + @JsonBackReference @NotNull(groups = View.Persist.class) private User user; diff --git a/src/main/java/ru/javawebinar/topjava/model/User.java b/src/main/java/ru/javawebinar/topjava/model/User.java index 41cbd5db8..00e9d5265 100644 --- a/src/main/java/ru/javawebinar/topjava/model/User.java +++ b/src/main/java/ru/javawebinar/topjava/model/User.java @@ -1,14 +1,11 @@ package ru.javawebinar.topjava.model; +import com.fasterxml.jackson.annotation.JsonManagedReference; import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.annotations.ApiModelProperty; import org.hibernate.annotations.Cache; import org.hibernate.annotations.*; import org.hibernate.validator.constraints.Range; import org.springframework.util.CollectionUtils; -import ru.javawebinar.topjava.HasIdAndEmail; -import ru.javawebinar.topjava.View; -import ru.javawebinar.topjava.util.validation.NoHtml; import javax.persistence.Entity; import javax.persistence.NamedQueries; @@ -22,7 +19,7 @@ import javax.validation.constraints.Size; import java.util.*; -import static ru.javawebinar.topjava.util.UsersUtil.DEFAULT_CALORIES_PER_DAY; +import static ru.javawebinar.topjava.util.UserUtil.DEFAULT_CALORIES_PER_DAY; @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @NamedQueries({ @@ -32,7 +29,7 @@ }) @Entity @Table(name = "users") -public class User extends AbstractNamedEntity implements HasIdAndEmail { +public class User extends AbstractNamedEntity { public static final String DELETE = "User.delete"; public static final String BY_EMAIL = "User.getByEmail"; @@ -42,7 +39,6 @@ public class User extends AbstractNamedEntity implements HasIdAndEmail { @Email @NotBlank @Size(max = 128) - @NoHtml(groups = {View.Web.class}) // https://stackoverflow.com/questions/17480809 private String email; @Column(name = "password", nullable = false) @@ -62,11 +58,11 @@ public class User extends AbstractNamedEntity implements HasIdAndEmail { @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) +// @Fetch(FetchMode.SUBSELECT) @BatchSize(size = 200) @JoinColumn @OnDelete(action = OnDeleteAction.CASCADE) @@ -79,7 +75,7 @@ public class User extends AbstractNamedEntity implements HasIdAndEmail { @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")//, cascade = CascadeType.REMOVE, orphanRemoval = true) @OrderBy("dateTime DESC") @OnDelete(action = OnDeleteAction.CASCADE) //https://stackoverflow.com/a/44988100/548473 - @ApiModelProperty(hidden = true) + @JsonManagedReference private List meals; public User() { @@ -90,7 +86,7 @@ public User(User u) { } public User(Integer id, String name, String email, String password, int caloriesPerDay, Role... roles) { - this(id, name, email, password, caloriesPerDay, true, new Date(), List.of(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) { @@ -103,7 +99,6 @@ public User(Integer id, String name, String email, String password, int calories setRoles(roles); } - @Override public String getEmail() { return email; } 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 924a2b781..82c80dd7b 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcMealRepository.java @@ -11,7 +11,7 @@ import org.springframework.transaction.annotation.Transactional; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.repository.MealRepository; -import ru.javawebinar.topjava.util.validation.ValidationUtil; +import ru.javawebinar.topjava.util.ValidationUtil; import java.time.LocalDateTime; import java.util.List; @@ -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 4074ff2c7..dda61a5f4 100644 --- a/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java +++ b/src/main/java/ru/javawebinar/topjava/repository/jdbc/JdbcUserRepository.java @@ -13,7 +13,7 @@ import ru.javawebinar.topjava.model.Role; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; -import ru.javawebinar.topjava.util.validation.ValidationUtil; +import ru.javawebinar.topjava.util.ValidationUtil; import java.util.*; @@ -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 b4ebae213..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.validation.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 e4aaed9ea..0946273f6 100644 --- a/src/main/java/ru/javawebinar/topjava/service/UserService.java +++ b/src/main/java/ru/javawebinar/topjava/service/UserService.java @@ -1,11 +1,9 @@ package ru.javawebinar.topjava.service; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.core.env.Environment; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; @@ -13,18 +11,16 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import ru.javawebinar.topjava.AuthorizedUser; -import ru.javawebinar.topjava.Profiles; -import ru.javawebinar.topjava.model.AbstractBaseEntity; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.repository.UserRepository; import ru.javawebinar.topjava.to.UserTo; -import ru.javawebinar.topjava.util.UsersUtil; -import ru.javawebinar.topjava.util.exception.UpdateRestrictionException; +import ru.javawebinar.topjava.util.UserUtil; import java.util.List; -import static ru.javawebinar.topjava.util.UsersUtil.prepareToSave; -import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.UserUtil.prepareToSave; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFound; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNotFoundWithId; @Service("userService") @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) @@ -33,14 +29,6 @@ public class UserService implements UserDetailsService { private final UserRepository repository; private final PasswordEncoder passwordEncoder; - private boolean modificationRestriction; - - @Autowired - @SuppressWarnings("deprecation") - public void setEnvironment(Environment environment) { - modificationRestriction = environment.acceptsProfiles(Profiles.VDS); - } - public UserService(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; this.passwordEncoder = passwordEncoder; @@ -54,12 +42,11 @@ public User create(User user) { @CacheEvict(value = "users", allEntries = true) public void delete(int id) { - checkModificationAllowed(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) { @@ -75,24 +62,20 @@ public List getAll() { @CacheEvict(value = "users", allEntries = true) public void update(User user) { Assert.notNull(user, "user must not be null"); -// checkNotFound : check works only for JDBC, disabled - checkModificationAllowed(user.id()); +// checkNotFoundWithId : check works only for JDBC, disabled prepareAndSave(user); } - @CacheEvict(value = "users", allEntries = true) @Transactional public void update(UserTo userTo) { - checkModificationAllowed(userTo.id()); User user = get(userTo.id()); - prepareAndSave(UsersUtil.updateFromTo(user, userTo)); + prepareAndSave(UserUtil.updateFromTo(user, userTo)); } @CacheEvict(value = "users", allEntries = true) @Transactional public void enable(int id, boolean enabled) { - checkModificationAllowed(id); User user = get(id); user.setEnabled(enabled); repository.save(user); // !! need only for JDBC implementation @@ -112,12 +95,6 @@ private User prepareAndSave(User user) { } public User getWithMeals(int id) { - return checkNotFound(repository.getWithMeals(id), id); - } - - protected void checkModificationAllowed(int id) { - if (modificationRestriction && id < AbstractBaseEntity.START_SEQ + 2) { - throw new UpdateRestrictionException(); - } + 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 index 7ccb8d970..b7a7de6b7 100644 --- a/src/main/java/ru/javawebinar/topjava/to/BaseTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/BaseTo.java @@ -1,10 +1,8 @@ package ru.javawebinar.topjava.to; -import io.swagger.annotations.ApiModelProperty; import ru.javawebinar.topjava.HasId; public abstract class BaseTo implements HasId { - @ApiModelProperty(hidden = true) protected Integer id; public BaseTo() { diff --git a/src/main/java/ru/javawebinar/topjava/to/UserTo.java b/src/main/java/ru/javawebinar/topjava/to/UserTo.java index 4fc5aec92..6f63cfa59 100644 --- a/src/main/java/ru/javawebinar/topjava/to/UserTo.java +++ b/src/main/java/ru/javawebinar/topjava/to/UserTo.java @@ -1,9 +1,7 @@ package ru.javawebinar.topjava.to; import org.hibernate.validator.constraints.Range; -import ru.javawebinar.topjava.HasIdAndEmail; -import ru.javawebinar.topjava.util.UsersUtil; -import ru.javawebinar.topjava.util.validation.NoHtml; +import ru.javawebinar.topjava.util.UserUtil; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; @@ -12,28 +10,26 @@ import java.io.Serial; import java.io.Serializable; -public class UserTo extends BaseTo implements HasIdAndEmail, Serializable { +public class UserTo extends BaseTo implements Serializable { @Serial private static final long serialVersionUID = 1L; @NotBlank @Size(min = 2, max = 100) - @NoHtml private String name; @Email @NotBlank @Size(max = 100) - @NoHtml // https://stackoverflow.com/questions/17480809 private String email; @NotBlank - @Size(min = 5, max = 32) + @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 = UsersUtil.DEFAULT_CALORIES_PER_DAY; + private Integer caloriesPerDay = UserUtil.DEFAULT_CALORIES_PER_DAY; public UserTo() { } @@ -62,7 +58,6 @@ public void setName(String name) { this.name = name; } - @Override public String getEmail() { return email; } diff --git a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java index 09052faa5..1fb662b11 100644 --- a/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/DateTimeUtil.java @@ -7,6 +7,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; public class DateTimeUtil { public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm"; @@ -24,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/UsersUtil.java b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java similarity index 97% rename from src/main/java/ru/javawebinar/topjava/util/UsersUtil.java rename to src/main/java/ru/javawebinar/topjava/util/UserUtil.java index bdb4291f7..a0700dd49 100644 --- a/src/main/java/ru/javawebinar/topjava/util/UsersUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/UserUtil.java @@ -5,7 +5,7 @@ import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; -public class UsersUtil { +public class UserUtil { public static final int DEFAULT_CALORIES_PER_DAY = 2000; diff --git a/src/main/java/ru/javawebinar/topjava/util/validation/ValidationUtil.java b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java similarity index 70% rename from src/main/java/ru/javawebinar/topjava/util/validation/ValidationUtil.java rename to src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java index 12840a142..6c91f95fd 100644 --- a/src/main/java/ru/javawebinar/topjava/util/validation/ValidationUtil.java +++ b/src/main/java/ru/javawebinar/topjava/util/ValidationUtil.java @@ -1,16 +1,17 @@ -package ru.javawebinar.topjava.util.validation; +package ru.javawebinar.topjava.util; + -import org.slf4j.Logger; 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.ErrorType; import ru.javawebinar.topjava.util.exception.IllegalRequestDataException; import ru.javawebinar.topjava.util.exception.NotFoundException; -import javax.servlet.http.HttpServletRequest; import javax.validation.*; import java.util.Set; +import java.util.stream.Collectors; public class ValidationUtil { @@ -34,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); } @@ -54,7 +55,7 @@ public static void checkNotFound(boolean found, String msg) { } } - public static void checkIsNew(HasId bean) { + public static void checkNew(HasId bean) { if (!bean.isNew()) { throw new IllegalRequestDataException(bean + " must be new (id=null)"); } @@ -76,17 +77,11 @@ public static Throwable getRootCause(@NonNull Throwable t) { return rootCause != null ? rootCause : t; } - public static String getMessage(Throwable e) { - return e.getLocalizedMessage() != null ? e.getLocalizedMessage() : e.getClass().getName(); - } - - public static Throwable logAndGetRootCause(Logger log, HttpServletRequest req, Exception e, boolean logStackTrace, ErrorType errorType) { - Throwable rootCause = ValidationUtil.getRootCause(e); - if (logStackTrace) { - log.error(errorType + " at request " + req.getRequestURL(), rootCause); - } else { - log.warn("{} at request {}: {}", errorType, req.getRequestURL(), rootCause.toString()); - } - return rootCause; + 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/ApplicationException.java b/src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java deleted file mode 100644 index 83bf0fed5..000000000 --- a/src/main/java/ru/javawebinar/topjava/util/exception/ApplicationException.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.javawebinar.topjava.util.exception; - -public class ApplicationException extends RuntimeException { - - private final ErrorType type; - private final String msgCode; - - public ApplicationException(String msgCode, ErrorType type) { - this.msgCode = msgCode; - this.type = type; - } - - public String getMsgCode() { - return msgCode; - } - - public ErrorType getType() { - return type; - } -} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java index 6a1f7a16a..d43323590 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorInfo.java @@ -3,13 +3,11 @@ public class ErrorInfo { private final String url; private final ErrorType type; - private final String typeMessage; - private final String[] details; + private final String detail; - public ErrorInfo(CharSequence url, ErrorType type, String typeMessage, String... details) { + public ErrorInfo(CharSequence url, ErrorType type, String detail) { this.url = url.toString(); this.type = type; - this.typeMessage = typeMessage; - this.details = details; + 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 index 5c16cf8ff..c53a433bc 100644 --- a/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java +++ b/src/main/java/ru/javawebinar/topjava/util/exception/ErrorType.java @@ -1,28 +1,8 @@ package ru.javawebinar.topjava.util.exception; -import org.springframework.http.HttpStatus; - public enum ErrorType { - APP_ERROR("error.appError", HttpStatus.INTERNAL_SERVER_ERROR), - // http://stackoverflow.com/a/22358422/548473 - DATA_NOT_FOUND("error.dataNotFound", HttpStatus.UNPROCESSABLE_ENTITY), - DATA_ERROR("error.dataError", HttpStatus.CONFLICT), - VALIDATION_ERROR("error.validationError", HttpStatus.UNPROCESSABLE_ENTITY), - WRONG_REQUEST("error.wrongRequest", HttpStatus.BAD_REQUEST); - - private final String errorCode; - private final HttpStatus status; - - ErrorType(String errorCode, HttpStatus status) { - this.errorCode = errorCode; - this.status = status; - } - - public String getErrorCode() { - return errorCode; - } - - public HttpStatus getStatus() { - return status; - } -} \ No newline at end of file + APP_ERROR, + DATA_NOT_FOUND, + DATA_ERROR, + VALIDATION_ERROR +} diff --git a/src/main/java/ru/javawebinar/topjava/util/exception/UpdateRestrictionException.java b/src/main/java/ru/javawebinar/topjava/util/exception/UpdateRestrictionException.java deleted file mode 100644 index abf3d36fe..000000000 --- a/src/main/java/ru/javawebinar/topjava/util/exception/UpdateRestrictionException.java +++ /dev/null @@ -1,9 +0,0 @@ -package ru.javawebinar.topjava.util.exception; - -public class UpdateRestrictionException extends ApplicationException { - public static final String EXCEPTION_UPDATE_RESTRICTION = "exception.user.updateRestriction"; - - public UpdateRestrictionException() { - super(EXCEPTION_UPDATE_RESTRICTION, ErrorType.VALIDATION_ERROR); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/util/validation/NoHtml.java b/src/main/java/ru/javawebinar/topjava/util/validation/NoHtml.java deleted file mode 100644 index b1b2a3b87..000000000 --- a/src/main/java/ru/javawebinar/topjava/util/validation/NoHtml.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.javawebinar.topjava.util.validation; - -import javax.validation.Constraint; -import javax.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Documented -@Constraint(validatedBy = NoHtmlValidator.class) -@Target({METHOD, FIELD}) -@Retention(RUNTIME) -public @interface NoHtml { - String message() default "Unsafe html content"; - - Class[] groups() default {}; - - Class[] payload() default {}; -} diff --git a/src/main/java/ru/javawebinar/topjava/util/validation/NoHtmlValidator.java b/src/main/java/ru/javawebinar/topjava/util/validation/NoHtmlValidator.java deleted file mode 100644 index 26d52d7ef..000000000 --- a/src/main/java/ru/javawebinar/topjava/util/validation/NoHtmlValidator.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.javawebinar.topjava.util.validation; - -import org.jsoup.Jsoup; -import org.jsoup.safety.Safelist; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -public class NoHtmlValidator implements ConstraintValidator { - @Override - public boolean isValid(String value, ConstraintValidatorContext ctx) { - return value == null || Jsoup.isValid(value, Safelist.none()); - } -} diff --git a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java index 543595bd2..aa841a06f 100644 --- a/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/ExceptionInfoHandler.java @@ -2,93 +2,64 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.ResponseEntity; +import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.validation.BindException; 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.validation.ValidationUtil; -import ru.javawebinar.topjava.util.exception.*; +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 java.util.Map; import static ru.javawebinar.topjava.util.exception.ErrorType.*; @RestControllerAdvice(annotations = RestController.class) @Order(Ordered.HIGHEST_PRECEDENCE + 5) public class ExceptionInfoHandler { - private static final Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); - - public static final String EXCEPTION_DUPLICATE_EMAIL = "exception.user.duplicateEmail"; - public static final String EXCEPTION_DUPLICATE_DATETIME = "exception.meal.duplicateDateTime"; - - private static final Map CONSTRAINTS_I18N_MAP = Map.of( - "users_unique_email_idx", EXCEPTION_DUPLICATE_EMAIL, - "meal_unique_user_datetime_idx", EXCEPTION_DUPLICATE_DATETIME); - - private final MessageSourceAccessor messageSourceAccessor; - - public ExceptionInfoHandler(MessageSourceAccessor messageSourceAccessor) { - this.messageSourceAccessor = messageSourceAccessor; - } + private static Logger log = LoggerFactory.getLogger(ExceptionInfoHandler.class); + // http://stackoverflow.com/a/22358422/548473 + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ExceptionHandler(NotFoundException.class) - public ResponseEntity notFoundError(HttpServletRequest req, NotFoundException e) { + public ErrorInfo handleError(HttpServletRequest req, NotFoundException e) { return logAndGetErrorInfo(req, e, false, DATA_NOT_FOUND); } - @ExceptionHandler(ApplicationException.class) - public ResponseEntity updateRestrictionError(HttpServletRequest req, ApplicationException appEx) { - return logAndGetErrorInfo(req, appEx, false, appEx.getType(), messageSourceAccessor.getMessage(appEx.getMsgCode())); - } - + @ResponseStatus(HttpStatus.CONFLICT) // 409 @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity conflict(HttpServletRequest req, DataIntegrityViolationException e) { - String rootMsg = ValidationUtil.getRootCause(e).getMessage(); - if (rootMsg != null) { - String lowerCaseMsg = rootMsg.toLowerCase(); - for (Map.Entry entry : CONSTRAINTS_I18N_MAP.entrySet()) { - if (lowerCaseMsg.contains(entry.getKey())) { - return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, messageSourceAccessor.getMessage(entry.getValue())); - } - } - } + public ErrorInfo conflict(HttpServletRequest req, DataIntegrityViolationException e) { return logAndGetErrorInfo(req, e, true, DATA_ERROR); } - @ExceptionHandler(BindException.class) - public ResponseEntity bindValidationError(HttpServletRequest req, BindException e) { - String[] details = e.getBindingResult().getFieldErrors().stream() - .map(messageSourceAccessor::getMessage) - .toArray(String[]::new); - - return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR, details); - } - + @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) // 422 @ExceptionHandler({IllegalRequestDataException.class, MethodArgumentTypeMismatchException.class, HttpMessageNotReadableException.class}) - public ResponseEntity validationError(HttpServletRequest req, Exception e) { + public ErrorInfo illegalRequestDataError(HttpServletRequest req, Exception e) { return logAndGetErrorInfo(req, e, false, VALIDATION_ERROR); } + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) - public ResponseEntity internalError(HttpServletRequest req, Exception e) { + 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 ResponseEntity logAndGetErrorInfo(HttpServletRequest req, Exception e, boolean logStackTrace, ErrorType errorType, String... details) { - Throwable rootCause = ValidationUtil.logAndGetRootCause(log, req, e, logStackTrace, errorType); - return ResponseEntity.status(errorType.getStatus()) - .body(new ErrorInfo(req.getRequestURL(), errorType, - messageSourceAccessor.getMessage(errorType.getErrorCode()), - details.length != 0 ? details : new String[]{ValidationUtil.getMessage(rootCause)}) - ); + 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 index 521985ae0..bd401d1c1 100644 --- a/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java +++ b/src/main/java/ru/javawebinar/topjava/web/GlobalExceptionHandler.java @@ -2,14 +2,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.support.MessageSourceAccessor; +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 org.springframework.web.servlet.NoHandlerFoundException; -import ru.javawebinar.topjava.util.exception.ApplicationException; -import ru.javawebinar.topjava.util.exception.ErrorType; -import ru.javawebinar.topjava.util.validation.ValidationUtil; +import ru.javawebinar.topjava.AuthorizedUser; +import ru.javawebinar.topjava.util.ValidationUtil; import javax.servlet.http.HttpServletRequest; import java.util.Map; @@ -18,36 +16,21 @@ public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - private final MessageSourceAccessor messageSourceAccessor; - - public GlobalExceptionHandler(MessageSourceAccessor messageSourceAccessor) { - this.messageSourceAccessor = messageSourceAccessor; - } - - @ExceptionHandler(NoHandlerFoundException.class) - public ModelAndView wrongRequest(HttpServletRequest req, NoHandlerFoundException e) { - return logAndGetExceptionView(req, e, false, ErrorType.WRONG_REQUEST, null); - } - - @ExceptionHandler(ApplicationException.class) - public ModelAndView updateRestrictionException(HttpServletRequest req, ApplicationException appEx) { - return logAndGetExceptionView(req, appEx, false, appEx.getType(), appEx.getMsgCode()); - } - @ExceptionHandler(Exception.class) public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { log.error("Exception at request " + req.getRequestURL(), e); - return logAndGetExceptionView(req, e, true, ErrorType.APP_ERROR, null); - } - - private ModelAndView logAndGetExceptionView(HttpServletRequest req, Exception e, boolean logException, ErrorType errorType, String code) { - Throwable rootCause = ValidationUtil.logAndGetRootCause(log, req, e, logException, errorType); + Throwable rootCause = ValidationUtil.getRootCause(e); + HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR; ModelAndView mav = new ModelAndView("exception", - Map.of("exception", rootCause, "message", code != null ? messageSourceAccessor.getMessage(code) : ValidationUtil.getMessage(rootCause), - "typeMessage", messageSourceAccessor.getMessage(errorType.getErrorCode()), - "status", errorType.getStatus())); - mav.setStatus(errorType.getStatus()); + 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 c6fbe3a78..d42b390d5 100644 --- a/src/main/java/ru/javawebinar/topjava/web/RootController.java +++ b/src/main/java/ru/javawebinar/topjava/web/RootController.java @@ -5,9 +5,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import springfox.documentation.annotations.ApiIgnore; -@ApiIgnore @Controller public class RootController { private static final Logger log = LoggerFactory.getLogger(RootController.class); diff --git a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java index 15448ce8c..bc4409869 100644 --- a/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java +++ b/src/main/java/ru/javawebinar/topjava/web/converter/DateTimeFormatters.java @@ -7,12 +7,15 @@ 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 LocalDate.parse(text); + return parseLocalDate(text); } @Override @@ -25,7 +28,7 @@ public static class LocalTimeFormatter implements Formatter { @Override public LocalTime parse(String text, Locale locale) { - return LocalTime.parse(text); + return parseLocalTime(text); } @Override 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/meal/AbstractMealController.java b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java index 63d9f9415..ec601c187 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/AbstractMealController.java @@ -14,8 +14,8 @@ import java.time.LocalTime; import java.util.List; -import static ru.javawebinar.topjava.util.validation.ValidationUtil.assureIdConsistent; -import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkIsNew; +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; public abstract class AbstractMealController { private final Logger log = LoggerFactory.getLogger(getClass()); @@ -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/MealRestController.java b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java index 0d3eb7e83..af1da8138 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealRestController.java @@ -4,10 +4,8 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealTo; @@ -43,12 +41,12 @@ public List getAll() { @Override @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) - public void update(@Validated(View.Web.class) @RequestBody Meal meal, @PathVariable int id) { + public void update(@RequestBody Meal meal, @PathVariable int id) { super.update(meal, id); } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWithLocation(@Validated(View.Web.class) @RequestBody Meal meal) { + public ResponseEntity createWithLocation(@RequestBody Meal meal) { Meal created = super.create(meal); URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() @@ -58,7 +56,6 @@ public ResponseEntity createWithLocation(@Validated(View.Web.class) @Reque return ResponseEntity.created(uriOfNewResource).body(created); } - @Override @GetMapping("/filter") public List getBetween( @RequestParam @Nullable LocalDate startDate, @@ -67,4 +64,4 @@ public List getBetween( @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 index 28a59fca7..67715ce7b 100644 --- a/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/meal/MealUIController.java @@ -2,19 +2,19 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; -import org.springframework.validation.annotation.Validated; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; -import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.Meal; import ru.javawebinar.topjava.to.MealTo; -import springfox.documentation.annotations.ApiIgnore; +import ru.javawebinar.topjava.util.ValidationUtil; +import javax.validation.Valid; import java.time.LocalDate; import java.time.LocalTime; import java.util.List; -@ApiIgnore @RestController @RequestMapping(value = "/profile/meals", produces = MediaType.APPLICATION_JSON_VALUE) public class MealUIController extends AbstractMealController { @@ -26,7 +26,7 @@ public List getAll() { } @Override - @GetMapping("/{id}") + @GetMapping( "/{id}") public Meal get(@PathVariable int id) { return super.get(id); } @@ -40,12 +40,17 @@ public void delete(@PathVariable int id) { @PostMapping @ResponseStatus(HttpStatus.NO_CONTENT) - public void createOrUpdate(@Validated(View.Web.class) Meal meal) { + 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 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 77a09cdc6..532e17816 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AbstractUserController.java @@ -3,17 +3,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.InitBinder; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.service.UserService; import ru.javawebinar.topjava.to.UserTo; -import ru.javawebinar.topjava.util.UsersUtil; +import ru.javawebinar.topjava.util.UserUtil; import java.util.List; -import static ru.javawebinar.topjava.util.validation.ValidationUtil.assureIdConsistent; -import static ru.javawebinar.topjava.util.validation.ValidationUtil.checkIsNew; +import static ru.javawebinar.topjava.util.ValidationUtil.assureIdConsistent; +import static ru.javawebinar.topjava.util.ValidationUtil.checkNew; public abstract class AbstractUserController { protected final Logger log = LoggerFactory.getLogger(getClass()); @@ -21,14 +19,6 @@ public abstract class AbstractUserController { @Autowired private UserService service; - @Autowired - private UniqueMailValidator emailValidator; - - @InitBinder - protected void initBinder(WebDataBinder binder) { - binder.addValidators(emailValidator); - } - public List getAll() { log.info("getAll"); return service.getAll(); @@ -41,13 +31,13 @@ public User get(int id) { public User create(UserTo userTo) { log.info("create {}", userTo); - checkIsNew(userTo); - return service.create(UsersUtil.createNewFromTo(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); } 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 896b22050..dfc40e6c6 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminRestController.java @@ -3,10 +3,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.User; import java.net.URI; @@ -31,7 +29,7 @@ public User get(@PathVariable int id) { } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity createWithLocation(@Validated(View.Web.class) @RequestBody User user) { + public ResponseEntity createWithLocation(@RequestBody User user) { User created = super.create(user); URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() .path(REST_URL + "/{id}") @@ -49,7 +47,7 @@ public void delete(@PathVariable int id) { @Override @PutMapping(value = "/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) - public void update(@Validated(View.Web.class) @RequestBody User user, @PathVariable int id) { + public void update(@RequestBody User user, @PathVariable int id) { super.update(user, id); } diff --git a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java index af939854a..0f5218560 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/AdminUIController.java @@ -2,16 +2,16 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.validation.annotation.Validated; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; -import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; -import springfox.documentation.annotations.ApiIgnore; +import ru.javawebinar.topjava.util.ValidationUtil; +import javax.validation.Valid; import java.util.List; -@ApiIgnore @RestController @RequestMapping(value = "/admin/users", produces = MediaType.APPLICATION_JSON_VALUE) public class AdminUIController extends AbstractUserController { @@ -36,12 +36,18 @@ public void delete(@PathVariable int id) { } @PostMapping - public void createOrUpdate(@Validated(View.Web.class) UserTo userTo) { + @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 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 c33b8eb25..c99f2ccaa 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileRestController.java @@ -3,37 +3,34 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import ru.javawebinar.topjava.AuthorizedUser; -import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.model.User; import ru.javawebinar.topjava.to.UserTo; -import springfox.documentation.annotations.ApiIgnore; import java.net.URI; +import static ru.javawebinar.topjava.web.SecurityUtil.authUserId; + @RestController @RequestMapping(value = ProfileRestController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE) public class ProfileRestController extends AbstractUserController { static final String REST_URL = "/rest/profile"; @GetMapping - public User get(@AuthenticationPrincipal @ApiIgnore AuthorizedUser authUser) { - return super.get(authUser.getId()); + public User get() { + return super.get(authUserId()); } @DeleteMapping @ResponseStatus(HttpStatus.NO_CONTENT) - public void delete(@AuthenticationPrincipal @ApiIgnore AuthorizedUser authUser) { - super.delete(authUser.getId()); + public void delete() { + super.delete(authUserId()); } @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity register(@Validated(View.Web.class) @RequestBody UserTo userTo) { + public ResponseEntity register(@RequestBody UserTo userTo) { User created = super.create(userTo); URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath() .path(REST_URL).build().toUri(); @@ -42,8 +39,8 @@ public ResponseEntity register(@Validated(View.Web.class) @RequestBody Use @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.NO_CONTENT) - public void update(@Validated(View.Web.class) @RequestBody UserTo userTo, @ApiIgnore @AuthenticationPrincipal AuthorizedUser authUser) { - super.update(userTo, authUser.getId()); + public void update(@RequestBody UserTo userTo) { + super.update(userTo, authUserId()); } @GetMapping("/text") @@ -52,7 +49,7 @@ public String testUTF() { } @GetMapping("/with-meals") - public User getWithMeals( @ApiIgnore @AuthenticationPrincipal AuthorizedUser authUser) { - return super.getWithMeals(authUser.getId()); + 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 index e83ee01d1..657396a7d 100644 --- a/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java +++ b/src/main/java/ru/javawebinar/topjava/web/user/ProfileUIController.java @@ -1,39 +1,36 @@ package ru.javawebinar.topjava.web.user; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; -import org.springframework.validation.annotation.Validated; 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.AuthorizedUser; -import ru.javawebinar.topjava.View; import ru.javawebinar.topjava.to.UserTo; -import springfox.documentation.annotations.ApiIgnore; +import ru.javawebinar.topjava.web.SecurityUtil; + +import javax.validation.Valid; -@ApiIgnore @Controller @RequestMapping("/profile") public class ProfileUIController extends AbstractUserController { @GetMapping - public String profile(ModelMap model, @AuthenticationPrincipal AuthorizedUser authUser) { - model.addAttribute("userTo", authUser.getUserTo()); + public String profile() { return "profile"; } @PostMapping - public String updateProfile(@Validated(View.Web.class) UserTo userTo, BindingResult result, SessionStatus status, @AuthenticationPrincipal AuthorizedUser authUser) { + 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"; } - super.update(userTo, authUser.getId()); - authUser.setTo(userTo); - status.setComplete(); - return "redirect:/meals"; } @GetMapping("/register") @@ -44,13 +41,14 @@ public String register(ModelMap model) { } @PostMapping("/register") - public String saveRegister(@Validated(View.Web.class) UserTo userTo, BindingResult result, SessionStatus status, ModelMap model) { + 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(); } - 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/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java b/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java deleted file mode 100644 index d72b8aa5c..000000000 --- a/src/main/java/ru/javawebinar/topjava/web/user/UniqueMailValidator.java +++ /dev/null @@ -1,54 +0,0 @@ -package ru.javawebinar.topjava.web.user; - - -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Component; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.validation.Errors; -import ru.javawebinar.topjava.HasIdAndEmail; -import ru.javawebinar.topjava.model.User; -import ru.javawebinar.topjava.repository.UserRepository; -import ru.javawebinar.topjava.web.ExceptionInfoHandler; -import ru.javawebinar.topjava.web.SecurityUtil; - -import javax.servlet.http.HttpServletRequest; - -@Component -public class UniqueMailValidator implements org.springframework.validation.Validator { - - private final UserRepository repository; - private final HttpServletRequest request; - - public UniqueMailValidator(UserRepository repository, @Nullable HttpServletRequest request) { - this.repository = repository; - this.request = request; - } - - @Override - public boolean supports(Class clazz) { - return HasIdAndEmail.class.isAssignableFrom(clazz); - } - - @Override - public void validate(Object target, Errors errors) { - HasIdAndEmail user = ((HasIdAndEmail) target); - if (StringUtils.hasText(user.getEmail())) { - User dbUser = repository.getByEmail(user.getEmail().toLowerCase()); - if (dbUser != null) { - Assert.notNull(request, "HttpServletRequest missed"); - if (request.getMethod().equals("PUT") || (request.getMethod().equals("POST") && user.getId() != null)) { // update for REST(PUT) and UI(POST) - int dbId = dbUser.id(); - // it is ok, if update ourselves - if (user.getId() != null && dbId == user.id()) return; - - // workaround for update with user.id=null in request body - // ValidationUtil.assureIdConsistent (id setter) called after this validation - String requestURI = request.getRequestURI(); - if (requestURI.endsWith("/" + dbId) || (dbId == SecurityUtil.get().getId() && requestURI.contains("/profile"))) return; - } - errors.rejectValue("email", ExceptionInfoHandler.EXCEPTION_DUPLICATE_EMAIL); - } - } - } -} 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 8d66cc0e5..8265d3655 100644 --- a/src/main/resources/db/populateDB.sql +++ b/src/main/resources/db/populateDB.sql @@ -1,5 +1,5 @@ -DELETE FROM user_role; -DELETE FROM meal; +DELETE FROM user_roles; +DELETE FROM meals; DELETE FROM users; ALTER SEQUENCE global_seq RESTART WITH 100000; @@ -8,12 +8,12 @@ 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 e2b565616..ab4cfe51e 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -30,4 +30,4 @@ -
\ 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 52c5d0df3..9c090f3c5 100644 --- a/src/main/resources/spring/spring-db.xml +++ b/src/main/resources/spring/spring-db.xml @@ -35,15 +35,6 @@ - - - - - - - - - - - + @@ -30,9 +29,6 @@ - - - @@ -52,10 +48,6 @@ - - - - @@ -78,17 +70,7 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/main/resources/spring/spring-security.xml b/src/main/resources/spring/spring-security.xml index 8988177c4..efcda19fe 100644 --- a/src/main/resources/spring/spring-security.xml +++ b/src/main/resources/spring/spring-security.xml @@ -19,11 +19,6 @@ - - - - - diff --git a/src/main/webapp/WEB-INF/jsp/exception.jsp b/src/main/webapp/WEB-INF/jsp/exception.jsp index 00a590c87..90f84f4b1 100644 --- a/src/main/webapp/WEB-INF/jsp/exception.jsp +++ b/src/main/webapp/WEB-INF/jsp/exception.jsp @@ -12,7 +12,7 @@

${status}

-

${typeMessage}

+

${message}

diff --git a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp index b355dc254..62f05f9a4 100644 --- a/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp +++ b/src/main/webapp/WEB-INF/jsp/fragments/bodyHeader.jsp @@ -3,49 +3,28 @@ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %> -