From 928bfcaf430fd0e52aad5fa8693611388c27f43f Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Tue, 24 Nov 2020 16:45:08 +0300
Subject: [PATCH 01/27] 1_01_user_with_lombok
---
pom.xml | 8 +++++++
.../java/ru/javaops/bootjava/model/Role.java | 6 +++++
.../java/ru/javaops/bootjava/model/User.java | 23 +++++++++++++++++++
3 files changed, 37 insertions(+)
create mode 100644 src/main/java/ru/javaops/bootjava/model/Role.java
create mode 100644 src/main/java/ru/javaops/bootjava/model/User.java
diff --git a/pom.xml b/pom.xml
index ca66a72..5be7a42 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,6 +53,14 @@
org.springframework.boot
spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
diff --git a/src/main/java/ru/javaops/bootjava/model/Role.java b/src/main/java/ru/javaops/bootjava/model/Role.java
new file mode 100644
index 0000000..432dde8
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/model/Role.java
@@ -0,0 +1,6 @@
+package ru.javaops.bootjava.model;
+
+public enum Role {
+ ROLE_USER,
+ ROLE_ADMIN
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java
new file mode 100644
index 0000000..b475761
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/model/User.java
@@ -0,0 +1,23 @@
+package ru.javaops.bootjava.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Set;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+
+ private String email;
+
+ private String firstName;
+
+ private String lastName;
+
+ private String password;
+
+ private Set roles;
+}
\ No newline at end of file
From 12f1315fa0eb5253041bb7cdeed21f59b7934054 Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Tue, 24 Nov 2020 16:45:18 +0300
Subject: [PATCH 02/27] 2_01_data_jpa
---
pom.xml | 4 +++
.../bootjava/RestaurantVotingApplication.java | 17 +++++++++-
.../java/ru/javaops/bootjava/model/User.java | 33 +++++++++++++++----
.../bootjava/repository/UserRepository.java | 7 ++++
src/main/resources/application.properties | 9 +++++
5 files changed, 63 insertions(+), 7 deletions(-)
create mode 100644 src/main/java/ru/javaops/bootjava/repository/UserRepository.java
diff --git a/pom.xml b/pom.xml
index 5be7a42..bf8aa32 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,10 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
com.h2database
diff --git a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
index 3326420..61d8ff8 100644
--- a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
+++ b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
@@ -1,14 +1,29 @@
package ru.javaops.bootjava;
import lombok.AllArgsConstructor;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import ru.javaops.bootjava.model.Role;
+import ru.javaops.bootjava.model.User;
+import ru.javaops.bootjava.repository.UserRepository;
+
+import java.util.Set;
@SpringBootApplication
@AllArgsConstructor
-public class RestaurantVotingApplication {
+public class RestaurantVotingApplication implements ApplicationRunner {
+ private final UserRepository userRepository;
public static void main(String[] args) {
SpringApplication.run(RestaurantVotingApplication.class, args);
}
+
+ @Override
+ public void run(ApplicationArguments args) {
+ userRepository.save(new User("user@gmail.com", "User_First", "User_Last", "password", Set.of(Role.ROLE_USER)));
+ userRepository.save(new User("admin@javaops.ru", "Admin_First", "Admin_Last", "admin", Set.of(Role.ROLE_USER, Role.ROLE_ADMIN)));
+ System.out.println(userRepository.findAll());
+ }
}
diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java
index b475761..284f632 100644
--- a/src/main/java/ru/javaops/bootjava/model/User.java
+++ b/src/main/java/ru/javaops/bootjava/model/User.java
@@ -1,23 +1,44 @@
package ru.javaops.bootjava.model;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import lombok.*;
+import org.springframework.data.jpa.domain.AbstractPersistable;
+import javax.persistence.*;
+import javax.validation.constraints.Email;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
import java.util.Set;
-@Data
-@NoArgsConstructor
+@Entity
+@Table(name = "users")
+@Getter
+@Setter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
-public class User {
+@ToString(callSuper = true, exclude = {"password"})
+public class User extends AbstractPersistable {
+ @Column(name = "email", nullable = false, unique = true)
+ @Email
+ @NotEmpty
+ @Size(max = 128)
private String email;
+ @Column(name = "first_name")
+ @Size(max = 128)
private String firstName;
+ @Column(name = "last_name")
+ @Size(max = 128)
private String lastName;
+ @Column(name = "password")
+ @Size(max = 256)
private String password;
+ @Enumerated(EnumType.STRING)
+ @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "user_roles_unique")})
+ @Column(name = "role")
+ @ElementCollection(fetch = FetchType.EAGER)
private Set roles;
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
new file mode 100644
index 0000000..590c614
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
@@ -0,0 +1,7 @@
+package ru.javaops.bootjava.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import ru.javaops.bootjava.model.User;
+
+public interface UserRepository extends JpaRepository {
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index e69de29..3492ea9 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -0,0 +1,9 @@
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+# JPA
+spring.jpa.show-sql=true
+spring.jpa.open-in-view=false
+# http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations
+spring.jpa.properties.hibernate.default_batch_fetch_size=20
+spring.jpa.properties.hibernate.format_sql=true
+# https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc
+spring.jpa.properties.hibernate.jdbc.batch_size=20
From 778c0ac943f456eeeeedffe7acf29f8e630da6a2 Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Tue, 24 Nov 2020 16:45:30 +0300
Subject: [PATCH 03/27] 2_02_h2_init
---
pom.xml | 1 -
.../bootjava/RestaurantVotingApplication.java | 6 ----
.../ru/javaops/bootjava/config/AppConfig.java | 26 ++++++++++++++++
src/main/resources/application.properties | 9 ------
src/main/resources/application.yaml | 30 +++++++++++++++++++
src/main/resources/data.sql | 8 +++++
6 files changed, 64 insertions(+), 16 deletions(-)
create mode 100644 src/main/java/ru/javaops/bootjava/config/AppConfig.java
delete mode 100644 src/main/resources/application.properties
create mode 100644 src/main/resources/application.yaml
create mode 100644 src/main/resources/data.sql
diff --git a/pom.xml b/pom.xml
index bf8aa32..0fe4458 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,7 +36,6 @@
com.h2database
h2
- runtime
org.projectlombok
diff --git a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
index 61d8ff8..d3b1792 100644
--- a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
+++ b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
@@ -5,12 +5,8 @@
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import ru.javaops.bootjava.model.Role;
-import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.repository.UserRepository;
-import java.util.Set;
-
@SpringBootApplication
@AllArgsConstructor
public class RestaurantVotingApplication implements ApplicationRunner {
@@ -22,8 +18,6 @@ public static void main(String[] args) {
@Override
public void run(ApplicationArguments args) {
- userRepository.save(new User("user@gmail.com", "User_First", "User_Last", "password", Set.of(Role.ROLE_USER)));
- userRepository.save(new User("admin@javaops.ru", "Admin_First", "Admin_Last", "admin", Set.of(Role.ROLE_USER, Role.ROLE_ADMIN)));
System.out.println(userRepository.findAll());
}
}
diff --git a/src/main/java/ru/javaops/bootjava/config/AppConfig.java b/src/main/java/ru/javaops/bootjava/config/AppConfig.java
new file mode 100644
index 0000000..19dbc45
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/config/AppConfig.java
@@ -0,0 +1,26 @@
+package ru.javaops.bootjava.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.h2.tools.Server;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.sql.SQLException;
+
+@Configuration
+@Slf4j
+public class AppConfig {
+
+/*
+ @Bean(initMethod = "start", destroyMethod = "stop")
+ public Server h2WebServer() throws SQLException {
+ return Server.createWebServer("-web", "-webAllowOthers", "-webPort", "8082");
+ }
+*/
+
+ @Bean(initMethod = "start", destroyMethod = "stop")
+ public Server h2Server() throws SQLException {
+ log.info("Start H2 TCP server");
+ return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092");
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index 3492ea9..0000000
--- a/src/main/resources/application.properties
+++ /dev/null
@@ -1,9 +0,0 @@
-# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
-# JPA
-spring.jpa.show-sql=true
-spring.jpa.open-in-view=false
-# http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations
-spring.jpa.properties.hibernate.default_batch_fetch_size=20
-spring.jpa.properties.hibernate.format_sql=true
-# https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc
-spring.jpa.properties.hibernate.jdbc.batch_size=20
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
new file mode 100644
index 0000000..6c6343d
--- /dev/null
+++ b/src/main/resources/application.yaml
@@ -0,0 +1,30 @@
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+spring:
+ jpa:
+ show-sql: true
+ open-in-view: false
+ hibernate:
+ ddl-auto: create-drop
+ properties:
+ # http://docs.jboss.org/hibernate/orm/5.3/userguide/html_single/Hibernate_User_Guide.html#configurations
+ hibernate:
+ format_sql: true
+ default_batch_fetch_size: 20
+ # https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc
+ jdbc.batch_size: 20
+ id.new_generator_mappings: false
+ datasource:
+ # ImMemory
+ url: jdbc:h2:mem:voting
+ # tcp: jdbc:h2:tcp://localhost:9092/mem:voting
+ # Absolute path
+ # url: jdbc:h2:C:/projects/bootjava/restorant-voting/db/voting
+ # tcp: jdbc:h2:tcp://localhost:9092/C:/projects/bootjava/restorant-voting/db/voting
+ # Relative path form current dir
+ # url: jdbc:h2:./db/voting
+ # Relative path from home
+ # url: jdbc:h2:~/voting
+ # tcp: jdbc:h2:tcp://localhost:9092/~/voting
+ username: sa
+ password:
+ h2.console.enabled: true
\ No newline at end of file
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
new file mode 100644
index 0000000..0fe391f
--- /dev/null
+++ b/src/main/resources/data.sql
@@ -0,0 +1,8 @@
+INSERT INTO USERS (EMAIL, FIRST_NAME, LAST_NAME, PASSWORD)
+VALUES ('user@gmail.com', 'User_First', 'User_Last', 'password'),
+ ('admin@javaops.ru', 'Admin_First', 'Admin_Last', 'admin');
+
+INSERT INTO USER_ROLE (ROLE, USER_ID)
+VALUES ('ROLE_USER', 1),
+ ('ROLE_ADMIN', 2),
+ ('ROLE_USER', 2);
\ No newline at end of file
From c6417e3dbeb9ae7f3a018bd7a569a03a9acb7290 Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Tue, 24 Nov 2020 16:45:43 +0300
Subject: [PATCH 04/27] 2_03_model_query
---
.../bootjava/RestaurantVotingApplication.java | 2 +-
.../ru/javaops/bootjava/model/BaseEntity.java | 52 +++++++++++++++++++
.../java/ru/javaops/bootjava/model/User.java | 3 +-
.../bootjava/repository/UserRepository.java | 11 ++++
src/main/resources/application.yaml | 1 -
5 files changed, 65 insertions(+), 4 deletions(-)
create mode 100644 src/main/java/ru/javaops/bootjava/model/BaseEntity.java
diff --git a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
index d3b1792..fa56af5 100644
--- a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
+++ b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
@@ -18,6 +18,6 @@ public static void main(String[] args) {
@Override
public void run(ApplicationArguments args) {
- System.out.println(userRepository.findAll());
+ System.out.println(userRepository.findByLastNameContainingIgnoreCase("last"));
}
}
diff --git a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
new file mode 100644
index 0000000..4a697e5
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
@@ -0,0 +1,52 @@
+package ru.javaops.bootjava.model;
+
+import lombok.*;
+import org.springframework.data.domain.Persistable;
+import org.springframework.data.util.ProxyUtils;
+import org.springframework.util.Assert;
+
+import javax.persistence.*;
+
+@MappedSuperclass
+// https://stackoverflow.com/a/6084701/548473
+@Access(AccessType.FIELD)
+@Getter
+@Setter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@ToString
+public abstract class BaseEntity implements Persistable {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ protected Integer 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 id == null;
+ }
+
+ // https://stackoverflow.com/questions/1638723
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || !getClass().equals(ProxyUtils.getUserClass(o))) {
+ return false;
+ }
+ BaseEntity that = (BaseEntity) o;
+ return id != null && id.equals(that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id == null ? 0 : id;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java
index 284f632..575aaff 100644
--- a/src/main/java/ru/javaops/bootjava/model/User.java
+++ b/src/main/java/ru/javaops/bootjava/model/User.java
@@ -1,7 +1,6 @@
package ru.javaops.bootjava.model;
import lombok.*;
-import org.springframework.data.jpa.domain.AbstractPersistable;
import javax.persistence.*;
import javax.validation.constraints.Email;
@@ -16,7 +15,7 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@ToString(callSuper = true, exclude = {"password"})
-public class User extends AbstractPersistable {
+public class User extends BaseEntity {
@Column(name = "email", nullable = false, unique = true)
@Email
diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
index 590c614..f5d1f0e 100644
--- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
+++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
@@ -1,7 +1,18 @@
package ru.javaops.bootjava.repository;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.transaction.annotation.Transactional;
import ru.javaops.bootjava.model.User;
+import java.util.List;
+import java.util.Optional;
+
+@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository {
+
+ @Query("SELECT u FROM User u WHERE u.email = LOWER(:email)")
+ Optional findByEmailIgnoreCase(String email);
+
+ List findByLastNameContainingIgnoreCase(String lastName);
}
\ No newline at end of file
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 6c6343d..7a0d777 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -12,7 +12,6 @@ spring:
default_batch_fetch_size: 20
# https://stackoverflow.com/questions/21257819/what-is-the-difference-between-hibernate-jdbc-fetch-size-and-hibernate-jdbc-batc
jdbc.batch_size: 20
- id.new_generator_mappings: false
datasource:
# ImMemory
url: jdbc:h2:mem:voting
From ddba8635b0aa9726287de64ab350dfad59472dfc Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Tue, 24 Nov 2020 16:51:36 +0300
Subject: [PATCH 05/27] 3_01_jpa_data_rest
---
pom.xml | 12 +++++++++++-
.../javaops/bootjava/repository/UserRepository.java | 3 +++
src/main/resources/application.yaml | 7 ++++++-
3 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/pom.xml b/pom.xml
index 0fe4458..5aabae1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,17 @@
org.springframework.boot
spring-boot-starter-validation
-
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
com.h2database
h2
diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
index f5d1f0e..dc2a413 100644
--- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
+++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
@@ -2,6 +2,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.transaction.annotation.Transactional;
import ru.javaops.bootjava.model.User;
@@ -11,8 +12,10 @@
@Transactional(readOnly = true)
public interface UserRepository extends JpaRepository {
+ @RestResource(rel = "by-email", path = "by-email")
@Query("SELECT u FROM User u WHERE u.email = LOWER(:email)")
Optional findByEmailIgnoreCase(String email);
+ @RestResource(rel = "by-lastname", path = "by-lastname")
List findByLastNameContainingIgnoreCase(String lastName);
}
\ No newline at end of file
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 7a0d777..98beb56 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -26,4 +26,9 @@ spring:
# tcp: jdbc:h2:tcp://localhost:9092/~/voting
username: sa
password:
- h2.console.enabled: true
\ No newline at end of file
+ h2.console.enabled: true
+
+ data.rest:
+ # https://docs.spring.io/spring-data/rest/docs/current/reference/html/#getting-started.basic-settings
+ basePath: /api
+ returnBodyOnCreate: true
\ No newline at end of file
From 08321643a1a748d410a14aab8f8d7bdbb7586435 Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Tue, 24 Nov 2020 16:51:52 +0300
Subject: [PATCH 06/27] 3_02_jackson
---
src/main/java/ru/javaops/bootjava/model/BaseEntity.java | 2 ++
src/main/resources/application.yaml | 9 ++++++++-
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
index 4a697e5..72ed0fc 100644
--- a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
+++ b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
@@ -1,5 +1,6 @@
package ru.javaops.bootjava.model;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.springframework.data.domain.Persistable;
import org.springframework.data.util.ProxyUtils;
@@ -27,6 +28,7 @@ public int id() {
return id;
}
+ @JsonIgnore
@Override
public boolean isNew() {
return id == null;
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 98beb56..9700512 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -31,4 +31,11 @@ spring:
data.rest:
# https://docs.spring.io/spring-data/rest/docs/current/reference/html/#getting-started.basic-settings
basePath: /api
- returnBodyOnCreate: true
\ No newline at end of file
+ returnBodyOnCreate: true
+
+# Jackson Serialization Issue Resolver
+# jackson:
+# visibility.field: any
+# visibility.getter: none
+# visibility.setter: none
+# visibility.is-getter: none
\ No newline at end of file
From 820eb6e9cea7ce2316e5e69b3ea5e6c589227cce Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Tue, 24 Nov 2020 18:12:28 +0300
Subject: [PATCH 07/27] 4_01_add_security
---
pom.xml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/pom.xml b/pom.xml
index 5aabae1..04dd5c3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,11 @@
org.springframework.boot
spring-boot-starter-data-rest
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+
+ org.springdoc
+ springdoc-openapi-ui
+ ${springdoc.version}
+
+
+ org.springdoc
+ springdoc-openapi-data-rest
+ ${springdoc.version}
+
+
+ org.springdoc
+ springdoc-openapi-security
+ ${springdoc.version}
+
com.h2database
h2
@@ -58,7 +76,7 @@
true
-
+
org.springframework.boot
spring-boot-starter-test
diff --git a/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java b/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java
new file mode 100644
index 0000000..4f6293d
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java
@@ -0,0 +1,39 @@
+package ru.javaops.bootjava.config;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
+import io.swagger.v3.oas.annotations.info.Contact;
+import io.swagger.v3.oas.annotations.info.Info;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityScheme;
+import org.springdoc.core.GroupedOpenApi;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+//https://sabljakovich.medium.com/adding-basic-auth-authorization-option-to-openapi-swagger-documentation-java-spring-95abbede27e9
+@SecurityScheme(
+ name = "basicAuth",
+ type = SecuritySchemeType.HTTP,
+ scheme = "basic"
+)
+@OpenAPIDefinition(
+ info = @Info(
+ title = "REST API documentation",
+ version = "1.0",
+ description = "Приложение по курсу BootJava",
+ contact = @Contact(url = "https://javaops.ru/#contacts", name = "Grigory Kislin", email = "admin@javaops.ru")
+ ),
+ security = @SecurityRequirement(name = "basicAuth")
+)
+public class OpenApiConfig {
+
+ @Bean
+ public GroupedOpenApi api() {
+ return GroupedOpenApi.builder()
+ .group("REST API")
+ .pathsToMatch("/api/**")
+ .pathsToExclude("/api/profile/**")
+ .build();
+ }
+}
diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
index b808c1d..5c71376 100644
--- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
+++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
@@ -1,5 +1,6 @@
package ru.javaops.bootjava.repository;
+import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -11,6 +12,7 @@
import java.util.Optional;
@Transactional(readOnly = true)
+@Tag(name = "User Controller")
public interface UserRepository extends JpaRepository {
@RestResource(rel = "by-email", path = "by-email")
diff --git a/src/main/java/ru/javaops/bootjava/web/AccountController.java b/src/main/java/ru/javaops/bootjava/web/AccountController.java
index f26e562..ee5806e 100644
--- a/src/main/java/ru/javaops/bootjava/web/AccountController.java
+++ b/src/main/java/ru/javaops/bootjava/web/AccountController.java
@@ -1,5 +1,6 @@
package ru.javaops.bootjava.web;
+import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.rest.webmvc.RepositoryLinksResource;
@@ -38,6 +39,7 @@
@RequestMapping("/api/account")
@AllArgsConstructor
@Slf4j
+@Tag(name = "Account Controller")
public class AccountController implements RepresentationModelProcessor {
@SuppressWarnings("unchecked")
private static final RepresentationModelAssemblerSupport> ASSEMBLER =
From 0ab428565f3693eaca888d9abaab0bc991e0cb7a Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Fri, 16 Apr 2021 01:09:06 +0300
Subject: [PATCH 17/27] 6_02_fix_update
---
pom.xml | 2 +-
.../javaops/bootjava/RestaurantVotingApplication.java | 2 --
.../ru/javaops/bootjava/util/JsonDeserializers.java | 2 ++
.../java/ru/javaops/bootjava/util/ValidationUtil.java | 2 ++
.../ru/javaops/bootjava/web/AccountController.java | 6 +++---
.../bootjava/web/error/GlobalExceptionHandler.java | 10 ++++++++++
src/main/resources/application.yaml | 2 +-
7 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/pom.xml b/pom.xml
index 43ff567..d1ac589 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 2.4.0
+ 2.4.4
ru.javaops.bootjava
diff --git a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
index 3326420..ee6a1ed 100644
--- a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
+++ b/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
@@ -1,11 +1,9 @@
package ru.javaops.bootjava;
-import lombok.AllArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
-@AllArgsConstructor
public class RestaurantVotingApplication {
public static void main(String[] args) {
diff --git a/src/main/java/ru/javaops/bootjava/util/JsonDeserializers.java b/src/main/java/ru/javaops/bootjava/util/JsonDeserializers.java
index a567a40..153afb4 100644
--- a/src/main/java/ru/javaops/bootjava/util/JsonDeserializers.java
+++ b/src/main/java/ru/javaops/bootjava/util/JsonDeserializers.java
@@ -5,10 +5,12 @@
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
+import lombok.experimental.UtilityClass;
import ru.javaops.bootjava.config.WebSecurityConfig;
import java.io.IOException;
+@UtilityClass
public class JsonDeserializers {
// https://stackoverflow.com/a/60995048/548473
diff --git a/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java b/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java
index 5f709d4..4d5c29c 100644
--- a/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java
+++ b/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java
@@ -1,8 +1,10 @@
package ru.javaops.bootjava.util;
+import lombok.experimental.UtilityClass;
import ru.javaops.bootjava.error.IllegalRequestDataException;
import ru.javaops.bootjava.model.BaseEntity;
+@UtilityClass
public class ValidationUtil {
public static void checkNew(BaseEntity entity) {
diff --git a/src/main/java/ru/javaops/bootjava/web/AccountController.java b/src/main/java/ru/javaops/bootjava/web/AccountController.java
index ee5806e..b05cfb9 100644
--- a/src/main/java/ru/javaops/bootjava/web/AccountController.java
+++ b/src/main/java/ru/javaops/bootjava/web/AccountController.java
@@ -22,7 +22,7 @@
import javax.validation.Valid;
import java.net.URI;
-import java.util.Set;
+import java.util.EnumSet;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
@@ -65,12 +65,12 @@ public void delete(@AuthenticationPrincipal AuthUser authUser) {
userRepository.deleteById(authUser.id());
}
- @PostMapping(value = "/register", consumes = MediaTypes.HAL_JSON_VALUE)
+ @PostMapping(value = "/register", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.CREATED)
public ResponseEntity> register(@Valid @RequestBody User user) {
log.info("register {}", user);
ValidationUtil.checkNew(user);
- user.setRoles(Set.of(Role.USER));
+ user.setRoles(EnumSet.of(Role.USER));
user = userRepository.save(user);
URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
.path("/api/account")
diff --git a/src/main/java/ru/javaops/bootjava/web/error/GlobalExceptionHandler.java b/src/main/java/ru/javaops/bootjava/web/error/GlobalExceptionHandler.java
index 9a917cb..d441662 100644
--- a/src/main/java/ru/javaops/bootjava/web/error/GlobalExceptionHandler.java
+++ b/src/main/java/ru/javaops/bootjava/web/error/GlobalExceptionHandler.java
@@ -1,7 +1,9 @@
package ru.javaops.bootjava.web.error;
import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -14,15 +16,23 @@
@RestControllerAdvice
@AllArgsConstructor
+@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private final ErrorAttributes errorAttributes;
@ExceptionHandler(AppException.class)
public ResponseEntity
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
diff --git a/src/main/java/ru/javaops/bootjava/web/AccountController.java b/src/main/java/ru/javaops/bootjava/web/AccountController.java
index b05cfb9..e13c485 100644
--- a/src/main/java/ru/javaops/bootjava/web/AccountController.java
+++ b/src/main/java/ru/javaops/bootjava/web/AccountController.java
@@ -36,11 +36,13 @@
* RequestMapping("/${spring.data.rest.basePath}/account") give "Not enough variable values"
*/
@RestController
-@RequestMapping("/api/account")
+@RequestMapping(AccountController.URL)
@AllArgsConstructor
@Slf4j
@Tag(name = "Account Controller")
public class AccountController implements RepresentationModelProcessor {
+ static final String URL = "/api/account";
+
@SuppressWarnings("unchecked")
private static final RepresentationModelAssemblerSupport> ASSEMBLER =
new RepresentationModelAssemblerSupport<>(AccountController.class, (Class>) (Class>) EntityModel.class) {
diff --git a/src/test/java/ru/javaops/bootjava/RestaurantVotingApplicationTests.java b/src/test/java/ru/javaops/bootjava/RestaurantVotingApplicationTests.java
deleted file mode 100644
index 52bba6d..0000000
--- a/src/test/java/ru/javaops/bootjava/RestaurantVotingApplicationTests.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package ru.javaops.bootjava;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class RestaurantVotingApplicationTests {
-
- @Test
- void contextLoads() {
- }
-}
diff --git a/src/test/java/ru/javaops/bootjava/UserTestUtil.java b/src/test/java/ru/javaops/bootjava/UserTestUtil.java
new file mode 100644
index 0000000..8e2123a
--- /dev/null
+++ b/src/test/java/ru/javaops/bootjava/UserTestUtil.java
@@ -0,0 +1,8 @@
+package ru.javaops.bootjava;
+
+public class UserTestUtil {
+ public static final int USER_ID = 1;
+ public static final int ADMIN_ID = 2;
+ public static final String USER_MAIL = "user@gmail.com";
+ public static final String ADMIN_MAIL = "admin@javaops.ru";
+}
diff --git a/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java b/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java
new file mode 100644
index 0000000..b5a26c0
--- /dev/null
+++ b/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java
@@ -0,0 +1,24 @@
+package ru.javaops.bootjava.web;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.transaction.annotation.Transactional;
+
+//https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications
+@SpringBootTest
+@Transactional
+@AutoConfigureMockMvc
+//https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment
+public abstract class AbstractControllerTest {
+
+ @Autowired
+ protected MockMvc mockMvc;
+
+ protected ResultActions perform(MockHttpServletRequestBuilder builder) throws Exception {
+ return mockMvc.perform(builder);
+ }
+}
diff --git a/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java b/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java
new file mode 100644
index 0000000..c977623
--- /dev/null
+++ b/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java
@@ -0,0 +1,45 @@
+package ru.javaops.bootjava.web;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import ru.javaops.bootjava.repository.UserRepository;
+
+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.javaops.bootjava.UserTestUtil.*;
+import static ru.javaops.bootjava.web.AccountController.URL;
+
+class AccountControllerTest extends AbstractControllerTest {
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void get() throws Exception {
+ perform(MockMvcRequestBuilders.get(URL))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE));
+ }
+
+ @Test
+ void getUnAuth() throws Exception {
+ perform(MockMvcRequestBuilders.get(URL))
+ .andExpect(status().isUnauthorized());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(URL))
+ .andExpect(status().isNoContent());
+ Assertions.assertFalse(userRepository.findById(USER_ID).isPresent());
+ Assertions.assertTrue(userRepository.findById(ADMIN_ID).isPresent());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java b/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java
new file mode 100644
index 0000000..c90354e
--- /dev/null
+++ b/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java
@@ -0,0 +1,64 @@
+package ru.javaops.bootjava.web;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import ru.javaops.bootjava.repository.UserRepository;
+
+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.javaops.bootjava.UserTestUtil.*;
+
+class UserControllerTest extends AbstractControllerTest {
+ static final String URL = "/api/users/";
+
+ @Autowired
+ private UserRepository userRepository;
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void get() throws Exception {
+ perform(MockMvcRequestBuilders.get(URL + USER_ID))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE));
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void getAll() throws Exception {
+ perform(MockMvcRequestBuilders.get(URL))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE));
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void getByEmail() throws Exception {
+ perform(MockMvcRequestBuilders.get(URL + "search/by-email?email=" + ADMIN_MAIL))
+ .andExpect(status().isOk())
+ .andDo(print())
+ .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE));
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void getForbidden() throws Exception {
+ perform(MockMvcRequestBuilders.get(URL))
+ .andExpect(status().isForbidden());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void delete() throws Exception {
+ perform(MockMvcRequestBuilders.delete(URL + USER_ID))
+ .andExpect(status().isNoContent());
+ Assertions.assertFalse(userRepository.findById(USER_ID).isPresent());
+ Assertions.assertTrue(userRepository.findById(ADMIN_ID).isPresent());
+ }
+}
\ No newline at end of file
From 1f1123d303b16755682ce4847245673dae358f0d Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Fri, 16 Apr 2021 01:10:00 +0300
Subject: [PATCH 19/27] 6_04_json_support
---
.../bootjava/config/WebSecurityConfig.java | 9 ++++++
.../java/ru/javaops/bootjava/model/User.java | 6 ++++
.../ru/javaops/bootjava/util/JsonUtil.java | 31 +++++++++++++++++++
.../ru/javaops/bootjava/UserTestUtil.java | 13 ++++++++
.../bootjava/web/AccountControllerTest.java | 24 ++++++++++++++
.../bootjava/web/UserControllerTest.java | 24 ++++++++++++++
6 files changed, 107 insertions(+)
create mode 100644 src/main/java/ru/javaops/bootjava/util/JsonUtil.java
diff --git a/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java b/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
index b6438fc..29fe8ee 100644
--- a/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
+++ b/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
@@ -1,5 +1,6 @@
package ru.javaops.bootjava.config;
+import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -18,7 +19,9 @@
import ru.javaops.bootjava.model.Role;
import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.repository.UserRepository;
+import ru.javaops.bootjava.util.JsonUtil;
+import javax.annotation.PostConstruct;
import java.util.Optional;
@Configuration
@@ -29,6 +32,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();
private final UserRepository userRepository;
+ private final ObjectMapper objectMapper;
+
+ @PostConstruct
+ void setMapper() {
+ JsonUtil.setObjectMapper(objectMapper);
+ }
@Bean
public UserDetailsService userDetailsService() {
diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java
index c61b200..effb0b1 100644
--- a/src/main/java/ru/javaops/bootjava/model/User.java
+++ b/src/main/java/ru/javaops/bootjava/model/User.java
@@ -11,6 +11,8 @@
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.io.Serializable;
+import java.util.Collection;
+import java.util.EnumSet;
import java.util.Set;
@Entity
@@ -21,6 +23,10 @@
@AllArgsConstructor
@ToString(callSuper = true, exclude = {"password"})
public class User extends BaseEntity implements Serializable {
+ public User(Integer id, String email, String firstName, String lastName, String password, Collection roles) {
+ this(email, firstName, lastName, password, EnumSet.copyOf(roles));
+ this.id = id;
+ }
@Column(name = "email", nullable = false, unique = true)
@Email
diff --git a/src/main/java/ru/javaops/bootjava/util/JsonUtil.java b/src/main/java/ru/javaops/bootjava/util/JsonUtil.java
new file mode 100644
index 0000000..336088a
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/util/JsonUtil.java
@@ -0,0 +1,31 @@
+package ru.javaops.bootjava.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import lombok.experimental.UtilityClass;
+
+import java.io.IOException;
+import java.util.List;
+
+@UtilityClass
+public class JsonUtil {
+ private static ObjectMapper objectMapper;
+
+ public static void setObjectMapper(ObjectMapper objectMapper) {
+ JsonUtil.objectMapper = objectMapper;
+ }
+
+ public static List readValues(String json, Class clazz) throws IOException {
+ ObjectReader reader = objectMapper.readerFor(clazz);
+ return reader.readValues(json).readAll();
+ }
+
+ public static T readValue(String json, Class clazz) throws JsonProcessingException {
+ return objectMapper.readValue(json, clazz);
+ }
+
+ public static String writeValue(T obj) throws JsonProcessingException {
+ return objectMapper.writeValueAsString(obj);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/UserTestUtil.java b/src/test/java/ru/javaops/bootjava/UserTestUtil.java
index 8e2123a..1a6bb63 100644
--- a/src/test/java/ru/javaops/bootjava/UserTestUtil.java
+++ b/src/test/java/ru/javaops/bootjava/UserTestUtil.java
@@ -1,8 +1,21 @@
package ru.javaops.bootjava;
+import ru.javaops.bootjava.model.Role;
+import ru.javaops.bootjava.model.User;
+
+import java.util.List;
+
public class UserTestUtil {
public static final int USER_ID = 1;
public static final int ADMIN_ID = 2;
public static final String USER_MAIL = "user@gmail.com";
public static final String ADMIN_MAIL = "admin@javaops.ru";
+
+ public static User getNew() {
+ return new User(null, "new@gmail.com", "New_First", "New_Last", "newpass", List.of(Role.USER));
+ }
+
+ public static User getUpdated() {
+ return new User(USER_ID, "user_update@gmail.com", "User_First_Update", "User_Last_Update", "password_update", List.of(Role.USER));
+ }
}
diff --git a/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java b/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java
index c977623..aa6ead2 100644
--- a/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java
@@ -4,14 +4,18 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
+import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import ru.javaops.bootjava.UserTestUtil;
+import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.repository.UserRepository;
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.javaops.bootjava.UserTestUtil.*;
+import static ru.javaops.bootjava.util.JsonUtil.writeValue;
import static ru.javaops.bootjava.web.AccountController.URL;
class AccountControllerTest extends AbstractControllerTest {
@@ -42,4 +46,24 @@ void delete() throws Exception {
Assertions.assertFalse(userRepository.findById(USER_ID).isPresent());
Assertions.assertTrue(userRepository.findById(ADMIN_ID).isPresent());
}
+
+ @Test
+ void register() throws Exception {
+ User newUser = UserTestUtil.getNew();
+ perform(MockMvcRequestBuilders.post(URL + "/register")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(writeValue(newUser)))
+ .andExpect(status().isCreated());
+ }
+
+ @Test
+ @WithUserDetails(value = USER_MAIL)
+ void update() throws Exception {
+ User updated = UserTestUtil.getUpdated();
+ perform(MockMvcRequestBuilders.put(URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(writeValue(updated)))
+ .andDo(print())
+ .andExpect(status().isNoContent());
+ }
}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java b/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java
index c90354e..9950437 100644
--- a/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java
@@ -4,14 +4,18 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
+import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import ru.javaops.bootjava.UserTestUtil;
+import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.repository.UserRepository;
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.javaops.bootjava.UserTestUtil.*;
+import static ru.javaops.bootjava.util.JsonUtil.writeValue;
class UserControllerTest extends AbstractControllerTest {
static final String URL = "/api/users/";
@@ -61,4 +65,24 @@ void delete() throws Exception {
Assertions.assertFalse(userRepository.findById(USER_ID).isPresent());
Assertions.assertTrue(userRepository.findById(ADMIN_ID).isPresent());
}
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void create() throws Exception {
+ User newUser = UserTestUtil.getNew();
+ perform(MockMvcRequestBuilders.post(URL)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(writeValue(newUser)))
+ .andExpect(status().isCreated());
+ }
+
+ @Test
+ @WithUserDetails(value = ADMIN_MAIL)
+ void update() throws Exception {
+ User updated = UserTestUtil.getUpdated();
+ perform(MockMvcRequestBuilders.put(URL + USER_ID)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(writeValue(updated)))
+ .andExpect(status().isNoContent());
+ }
}
\ No newline at end of file
From 8d443637a0414297729d695b0eb4a9f10da54acf Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Fri, 16 Apr 2021 01:10:17 +0300
Subject: [PATCH 20/27] 6_05_test_body_check
---
.../ru/javaops/bootjava/UserTestUtil.java | 28 +++++++++++++++++++
.../bootjava/web/AccountControllerTest.java | 12 ++++++--
.../bootjava/web/UserControllerTest.java | 11 ++++++--
3 files changed, 45 insertions(+), 6 deletions(-)
diff --git a/src/test/java/ru/javaops/bootjava/UserTestUtil.java b/src/test/java/ru/javaops/bootjava/UserTestUtil.java
index 1a6bb63..c0c5c78 100644
--- a/src/test/java/ru/javaops/bootjava/UserTestUtil.java
+++ b/src/test/java/ru/javaops/bootjava/UserTestUtil.java
@@ -1,15 +1,25 @@
package ru.javaops.bootjava;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.ResultMatcher;
import ru.javaops.bootjava.model.Role;
import ru.javaops.bootjava.model.User;
+import ru.javaops.bootjava.util.JsonUtil;
+import java.io.UnsupportedEncodingException;
import java.util.List;
+import java.util.function.BiConsumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
public class UserTestUtil {
public static final int USER_ID = 1;
public static final int ADMIN_ID = 2;
public static final String USER_MAIL = "user@gmail.com";
public static final String ADMIN_MAIL = "admin@javaops.ru";
+ public static final User user = new User(USER_ID, USER_MAIL, "User_First", "User_Last", "password", List.of(Role.USER));
+ public static final User admin = new User(ADMIN_ID, ADMIN_MAIL, "Admin_First", "Admin_Last", "admin", List.of(Role.ADMIN, Role.USER));
public static User getNew() {
return new User(null, "new@gmail.com", "New_First", "New_Last", "newpass", List.of(Role.USER));
@@ -18,4 +28,22 @@ public static User getNew() {
public static User getUpdated() {
return new User(USER_ID, "user_update@gmail.com", "User_First_Update", "User_Last_Update", "password_update", List.of(Role.USER));
}
+
+ public static void assertEquals(User actual, User expected) {
+ assertThat(actual).usingRecursiveComparison().ignoringFields("password").isEqualTo(expected);
+ }
+
+ // No id in HATEOAS answer
+ public static void assertNoIdEquals(User actual, User expected) {
+ assertThat(actual).usingRecursiveComparison().ignoringFields("id", "password").isEqualTo(expected);
+ }
+
+ public static User asUser(MvcResult mvcResult) throws UnsupportedEncodingException, JsonProcessingException {
+ String jsonActual = mvcResult.getResponse().getContentAsString();
+ return JsonUtil.readValue(jsonActual, User.class);
+ }
+
+ public static ResultMatcher jsonMatcher(User expected, BiConsumer equalsAssertion) {
+ return mvcResult -> equalsAssertion.accept(asUser(mvcResult), expected);
+ }
}
diff --git a/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java b/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java
index aa6ead2..9185be8 100644
--- a/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/web/AccountControllerTest.java
@@ -29,7 +29,8 @@ void get() throws Exception {
perform(MockMvcRequestBuilders.get(URL))
.andExpect(status().isOk())
.andDo(print())
- .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE));
+ .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE))
+ .andExpect(jsonMatcher(user, UserTestUtil::assertEquals));
}
@Test
@@ -50,10 +51,14 @@ void delete() throws Exception {
@Test
void register() throws Exception {
User newUser = UserTestUtil.getNew();
- perform(MockMvcRequestBuilders.post(URL + "/register")
+ User registered = asUser(perform(MockMvcRequestBuilders.post(URL + "/register")
.contentType(MediaType.APPLICATION_JSON)
.content(writeValue(newUser)))
- .andExpect(status().isCreated());
+ .andExpect(status().isCreated()).andReturn());
+ int newId = registered.id();
+ newUser.setId(newId);
+ UserTestUtil.assertEquals(registered, newUser);
+ UserTestUtil.assertEquals(registered, userRepository.findById(newId).orElseThrow());
}
@Test
@@ -65,5 +70,6 @@ void update() throws Exception {
.content(writeValue(updated)))
.andDo(print())
.andExpect(status().isNoContent());
+ UserTestUtil.assertEquals(updated, userRepository.findById(USER_ID).orElseThrow());
}
}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java b/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java
index 9950437..9295ca0 100644
--- a/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/web/UserControllerTest.java
@@ -29,12 +29,14 @@ void get() throws Exception {
perform(MockMvcRequestBuilders.get(URL + USER_ID))
.andExpect(status().isOk())
.andDo(print())
- .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE));
+ .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE))
+ .andExpect(jsonMatcher(user, UserTestUtil::assertNoIdEquals));
}
@Test
@WithUserDetails(value = ADMIN_MAIL)
void getAll() throws Exception {
+ // TODO check content yourself
perform(MockMvcRequestBuilders.get(URL))
.andExpect(status().isOk())
.andDo(print())
@@ -47,7 +49,8 @@ void getByEmail() throws Exception {
perform(MockMvcRequestBuilders.get(URL + "search/by-email?email=" + ADMIN_MAIL))
.andExpect(status().isOk())
.andDo(print())
- .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE));
+ .andExpect(content().contentTypeCompatibleWith(MediaTypes.HAL_JSON_VALUE))
+ .andExpect(jsonMatcher(admin, UserTestUtil::assertNoIdEquals));
}
@Test
@@ -73,7 +76,8 @@ void create() throws Exception {
perform(MockMvcRequestBuilders.post(URL)
.contentType(MediaType.APPLICATION_JSON)
.content(writeValue(newUser)))
- .andExpect(status().isCreated());
+ .andExpect(status().isCreated())
+ .andExpect(jsonMatcher(newUser, UserTestUtil::assertNoIdEquals));
}
@Test
@@ -84,5 +88,6 @@ void update() throws Exception {
.contentType(MediaType.APPLICATION_JSON)
.content(writeValue(updated)))
.andExpect(status().isNoContent());
+ UserTestUtil.assertEquals(updated, userRepository.findById(USER_ID).orElseThrow());
}
}
\ No newline at end of file
From 0d3faa24f4fac73db6b48f60195de5395dc7627b Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Fri, 16 Apr 2021 01:10:30 +0300
Subject: [PATCH 21/27] 6_06_add_cache
---
pom.xml | 11 +++++++++++
.../java/ru/javaops/bootjava/config/AppConfig.java | 2 ++
.../ru/javaops/bootjava/config/WebSecurityConfig.java | 2 +-
.../javaops/bootjava/repository/UserRepository.java | 2 ++
.../ru/javaops/bootjava/web/AccountController.java | 8 ++++++--
5 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/pom.xml b/pom.xml
index e7d5855..a78d752 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,6 +66,17 @@
springdoc-openapi-security
${springdoc.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-cache
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+
+
com.h2database
h2
diff --git a/src/main/java/ru/javaops/bootjava/config/AppConfig.java b/src/main/java/ru/javaops/bootjava/config/AppConfig.java
index 19dbc45..4a9742b 100644
--- a/src/main/java/ru/javaops/bootjava/config/AppConfig.java
+++ b/src/main/java/ru/javaops/bootjava/config/AppConfig.java
@@ -2,6 +2,7 @@
import lombok.extern.slf4j.Slf4j;
import org.h2.tools.Server;
+import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -9,6 +10,7 @@
@Configuration
@Slf4j
+@EnableCaching
public class AppConfig {
/*
diff --git a/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java b/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
index 29fe8ee..2e18324 100644
--- a/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
+++ b/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
@@ -43,7 +43,7 @@ void setMapper() {
public UserDetailsService userDetailsService() {
return email -> {
log.debug("Authenticating '{}'", email);
- Optional optionalUser = userRepository.findByEmailIgnoreCase(email);
+ Optional optionalUser = userRepository.findByEmailIgnoreCase(email.toLowerCase());
return new AuthUser(optionalUser.orElseThrow(
() -> new UsernameNotFoundException("User '" + email + "' was not found")));
};
diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
index 5c71376..b676a7f 100644
--- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
+++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
@@ -1,6 +1,7 @@
package ru.javaops.bootjava.repository;
import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -17,6 +18,7 @@ public interface UserRepository extends JpaRepository {
@RestResource(rel = "by-email", path = "by-email")
@Query("SELECT u FROM User u WHERE u.email = LOWER(:email)")
+ @Cacheable("users")
Optional findByEmailIgnoreCase(String email);
@RestResource(rel = "by-lastname", path = "by-lastname")
diff --git a/src/main/java/ru/javaops/bootjava/web/AccountController.java b/src/main/java/ru/javaops/bootjava/web/AccountController.java
index e13c485..a43eb3e 100644
--- a/src/main/java/ru/javaops/bootjava/web/AccountController.java
+++ b/src/main/java/ru/javaops/bootjava/web/AccountController.java
@@ -3,6 +3,8 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
import org.springframework.data.rest.webmvc.RepositoryLinksResource;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.MediaTypes;
@@ -62,6 +64,7 @@ public EntityModel get(@AuthenticationPrincipal AuthUser authUser) {
@DeleteMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
+ @CacheEvict(value = "users", key = "#authUser.username")
public void delete(@AuthenticationPrincipal AuthUser authUser) {
log.info("delete {}", authUser);
userRepository.deleteById(authUser.id());
@@ -82,7 +85,8 @@ public ResponseEntity> register(@Valid @RequestBody User user)
@PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
- public void update(@Valid @RequestBody User user, @AuthenticationPrincipal AuthUser authUser) {
+ @CachePut(value = "users", key = "#authUser.username")
+ public User update(@Valid @RequestBody User user, @AuthenticationPrincipal AuthUser authUser) {
log.info("update {} to {}", authUser, user);
User oldUser = authUser.getUser();
ValidationUtil.assureIdConsistent(user, oldUser.id());
@@ -90,7 +94,7 @@ public void update(@Valid @RequestBody User user, @AuthenticationPrincipal AuthU
if (user.getPassword() == null) {
user.setPassword(oldUser.getPassword());
}
- userRepository.save(user);
+ return userRepository.save(user);
}
/*
From 22e33769e12758c8473390c6fbbcbf33cc827b61 Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Fri, 16 Apr 2021 01:11:06 +0300
Subject: [PATCH 22/27] 6_07_update_cache
---
.../bootjava/repository/UserRepository.java | 21 +++++++++++++++++++
.../bootjava/web/AbstractControllerTest.java | 2 ++
src/test/resources/application-test.yaml | 1 +
3 files changed, 24 insertions(+)
create mode 100644 src/test/resources/application-test.yaml
diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
index b676a7f..a579ae6 100644
--- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
+++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
@@ -1,10 +1,13 @@
package ru.javaops.bootjava.repository;
import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.transaction.annotation.Transactional;
@@ -23,4 +26,22 @@ public interface UserRepository extends JpaRepository {
@RestResource(rel = "by-lastname", path = "by-lastname")
Page findByLastNameContainingIgnoreCase(String lastName, Pageable page);
+
+ @Override
+ @Modifying
+ @Transactional
+ @CachePut(value = "users", key = "#user.email")
+ User save(User user);
+
+ @Override
+ @Modifying
+ @Transactional
+ @CacheEvict(value = "users", key = "#user.email")
+ void delete(User user);
+
+ @Override
+ @Modifying
+ @Transactional
+ @CacheEvict(value = "users", allEntries = true)
+ void deleteById(Integer integer);
}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java b/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java
index b5a26c0..68cf79c 100644
--- a/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java
@@ -3,6 +3,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
@@ -12,6 +13,7 @@
@SpringBootTest
@Transactional
@AutoConfigureMockMvc
+@ActiveProfiles("test")
//https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-mock-environment
public abstract class AbstractControllerTest {
diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml
new file mode 100644
index 0000000..be16632
--- /dev/null
+++ b/src/test/resources/application-test.yaml
@@ -0,0 +1 @@
+spring.cache.type: none
\ No newline at end of file
From 85e582905371cc39d9991188625c90d5bef7ff0c Mon Sep 17 00:00:00 2001
From: "admin@javaops.ru"
Date: Wed, 27 Apr 2022 10:58:59 +0300
Subject: [PATCH 23/27] 7_01_upgrade_refactoring
---
pom.xml | 36 +++++---
...lication.java => BootJavaApplication.java} | 4 +-
.../ru/javaops/bootjava/config/AppConfig.java | 11 +--
.../bootjava/config/OpenApiConfig.java | 7 +-
.../bootjava/config/WebSecurityConfig.java | 29 ++++---
.../javaops/bootjava/error/AppException.java | 5 ++
.../ru/javaops/bootjava/model/BaseEntity.java | 8 +-
.../java/ru/javaops/bootjava/model/User.java | 16 ++--
.../util/{ => validation}/ValidationUtil.java | 11 ++-
.../bootjava/web/AccountController.java | 5 +-
.../javaops/bootjava/{ => web}/AuthUser.java | 2 +-
.../bootjava/web/GlobalExceptionHandler.java | 86 +++++++++++++++++++
.../web/error/GlobalExceptionHandler.java | 38 --------
src/main/resources/application.yaml | 23 +++--
.../bootjava/web/AbstractControllerTest.java | 2 +-
.../bootjava/web/AccountControllerTest.java | 9 +-
16 files changed, 189 insertions(+), 103 deletions(-)
rename src/main/java/ru/javaops/bootjava/{RestaurantVotingApplication.java => BootJavaApplication.java} (66%)
rename src/main/java/ru/javaops/bootjava/util/{ => validation}/ValidationUtil.java (67%)
rename src/main/java/ru/javaops/bootjava/{ => web}/AuthUser.java (93%)
create mode 100644 src/main/java/ru/javaops/bootjava/web/GlobalExceptionHandler.java
delete mode 100644 src/main/java/ru/javaops/bootjava/web/error/GlobalExceptionHandler.java
diff --git a/pom.xml b/pom.xml
index a78d752..3736dc5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,19 +5,21 @@
org.springframework.boot
spring-boot-starter-parent
- 2.4.4
+ 2.6.7
- ru.javaops.bootjava
- restaurant-voting
+ ru.javaops
+ bootjava
1.0
- restaurant-voting
+ BootJava
Spring Boot 2.x HATEOAS application (BootJava)
https://javaops.ru/view/bootjava
- 15
- 1.5.6
+ 17
+ 1.6.8
+ UTF-8
+ UTF-8
@@ -42,14 +44,6 @@
spring-boot-starter-security
-
-
org.springdoc
@@ -99,6 +93,12 @@
spring-security-test
test
+
+
+ org.junit.platform
+ junit-platform-launcher
+ test
+
@@ -115,6 +115,14 @@
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ -Dfile.encoding=UTF-8
+
+
diff --git a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java b/src/main/java/ru/javaops/bootjava/BootJavaApplication.java
similarity index 66%
rename from src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
rename to src/main/java/ru/javaops/bootjava/BootJavaApplication.java
index ee6a1ed..fd874f1 100644
--- a/src/main/java/ru/javaops/bootjava/RestaurantVotingApplication.java
+++ b/src/main/java/ru/javaops/bootjava/BootJavaApplication.java
@@ -4,9 +4,9 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
-public class RestaurantVotingApplication {
+public class BootJavaApplication {
public static void main(String[] args) {
- SpringApplication.run(RestaurantVotingApplication.class, args);
+ SpringApplication.run(BootJavaApplication.class, args);
}
}
diff --git a/src/main/java/ru/javaops/bootjava/config/AppConfig.java b/src/main/java/ru/javaops/bootjava/config/AppConfig.java
index 4a9742b..cf1e5e7 100644
--- a/src/main/java/ru/javaops/bootjava/config/AppConfig.java
+++ b/src/main/java/ru/javaops/bootjava/config/AppConfig.java
@@ -5,6 +5,7 @@
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
import java.sql.SQLException;
@@ -13,15 +14,9 @@
@EnableCaching
public class AppConfig {
-/*
+ @Profile("!test")
@Bean(initMethod = "start", destroyMethod = "stop")
- public Server h2WebServer() throws SQLException {
- return Server.createWebServer("-web", "-webAllowOthers", "-webPort", "8082");
- }
-*/
-
- @Bean(initMethod = "start", destroyMethod = "stop")
- public Server h2Server() throws SQLException {
+ Server h2Server() throws SQLException {
log.info("Start H2 TCP server");
return Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092");
}
diff --git a/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java b/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java
index 4f6293d..8445b18 100644
--- a/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java
+++ b/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java
@@ -21,7 +21,12 @@
info = @Info(
title = "REST API documentation",
version = "1.0",
- description = "Приложение по курсу BootJava",
+ description = """
+ Приложение по курсу BootJava
+ Тестовые креденшелы:
+ - user@gmail.com / password
+ - admin@javaops.ru / admin
+ """,
contact = @Contact(url = "https://javaops.ru/#contacts", name = "Grigory Kislin", email = "admin@javaops.ru")
),
security = @SecurityRequirement(name = "basicAuth")
diff --git a/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java b/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
index 2e18324..76b25a3 100644
--- a/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
+++ b/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
@@ -6,6 +6,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@@ -15,13 +16,12 @@
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
-import ru.javaops.bootjava.AuthUser;
import ru.javaops.bootjava.model.Role;
import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.repository.UserRepository;
import ru.javaops.bootjava.util.JsonUtil;
+import ru.javaops.bootjava.web.AuthUser;
-import javax.annotation.PostConstruct;
import java.util.Optional;
@Configuration
@@ -32,33 +32,34 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();
private final UserRepository userRepository;
- private final ObjectMapper objectMapper;
- @PostConstruct
- void setMapper() {
+ @Autowired
+ private void setMapper(ObjectMapper objectMapper) {
JsonUtil.setObjectMapper(objectMapper);
}
@Bean
- public UserDetailsService userDetailsService() {
- return email -> {
- log.debug("Authenticating '{}'", email);
- Optional optionalUser = userRepository.findByEmailIgnoreCase(email.toLowerCase());
- return new AuthUser(optionalUser.orElseThrow(
- () -> new UsernameNotFoundException("User '" + email + "' was not found")));
- };
+ @Override
+ public UserDetailsService userDetailsServiceBean() throws Exception {
+ return super.userDetailsServiceBean();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
- auth.userDetailsService(userDetailsService())
+ auth.userDetailsService(
+ email -> {
+ log.debug("Authenticating '{}'", email);
+ Optional optionalUser = userRepository.findByEmailIgnoreCase(email);
+ return new AuthUser(optionalUser.orElseThrow(
+ () -> new UsernameNotFoundException("User '" + email + "' was not found")));
+ })
.passwordEncoder(PASSWORD_ENCODER);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
- .antMatchers("/api/account/register").anonymous()
+ .antMatchers(HttpMethod.POST, "/api/account").anonymous()
.antMatchers("/api/account").hasRole(Role.USER.name())
.antMatchers("/api/**").hasRole(Role.ADMIN.name())
.and().httpBasic()
diff --git a/src/main/java/ru/javaops/bootjava/error/AppException.java b/src/main/java/ru/javaops/bootjava/error/AppException.java
index 809caad..9d00dd4 100644
--- a/src/main/java/ru/javaops/bootjava/error/AppException.java
+++ b/src/main/java/ru/javaops/bootjava/error/AppException.java
@@ -13,4 +13,9 @@ public AppException(HttpStatus status, String message, ErrorAttributeOptions opt
super(status, message);
this.options = options;
}
+
+ @Override
+ public String getMessage() {
+ return getReason();
+ }
}
diff --git a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
index 72ed0fc..c942ac3 100644
--- a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
+++ b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
@@ -1,6 +1,7 @@
package ru.javaops.bootjava.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.springframework.data.domain.Persistable;
import org.springframework.data.util.ProxyUtils;
@@ -15,11 +16,11 @@
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
-@ToString
public abstract class BaseEntity implements Persistable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY) // https://stackoverflow.com/a/28025008/548473
protected Integer id;
// doesn't work for hibernate lazy proxy
@@ -51,4 +52,9 @@ public boolean equals(Object o) {
public int hashCode() {
return id == null ? 0 : id;
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ":" + id;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java
index effb0b1..4878c62 100644
--- a/src/main/java/ru/javaops/bootjava/model/User.java
+++ b/src/main/java/ru/javaops/bootjava/model/User.java
@@ -8,7 +8,7 @@
import javax.persistence.*;
import javax.validation.constraints.Email;
-import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.Collection;
@@ -21,16 +21,15 @@
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
-@ToString(callSuper = true, exclude = {"password"})
public class User extends BaseEntity implements Serializable {
public User(Integer id, String email, String firstName, String lastName, String password, Collection roles) {
- this(email, firstName, lastName, password, EnumSet.copyOf(roles));
+ this(email, firstName, lastName, password, roles.isEmpty() ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles));
this.id = id;
}
@Column(name = "email", nullable = false, unique = true)
@Email
- @NotEmpty
+ @NotBlank
@Size(max = 128)
private String email;
@@ -49,7 +48,9 @@ public User(Integer id, String email, String firstName, String lastName, String
private String password;
@Enumerated(EnumType.STRING)
- @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), uniqueConstraints = {@UniqueConstraint(columnNames = {"user_id", "role"}, name = "user_roles_unique")})
+ @CollectionTable(name = "user_role",
+ joinColumns = @JoinColumn(name = "user_id"),
+ uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role"))
@Column(name = "role")
@ElementCollection(fetch = FetchType.EAGER)
private Set roles;
@@ -57,4 +58,9 @@ public User(Integer id, String email, String firstName, String lastName, String
public void setEmail(String email) {
this.email = StringUtils.hasText(email) ? email.toLowerCase() : null;
}
+
+ @Override
+ public String toString() {
+ return "User:" + id + '[' + email + ']';
+ }
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java b/src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java
similarity index 67%
rename from src/main/java/ru/javaops/bootjava/util/ValidationUtil.java
rename to src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java
index 4d5c29c..991c447 100644
--- a/src/main/java/ru/javaops/bootjava/util/ValidationUtil.java
+++ b/src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java
@@ -1,6 +1,8 @@
-package ru.javaops.bootjava.util;
+package ru.javaops.bootjava.util.validation;
import lombok.experimental.UtilityClass;
+import org.springframework.core.NestedExceptionUtils;
+import org.springframework.lang.NonNull;
import ru.javaops.bootjava.error.IllegalRequestDataException;
import ru.javaops.bootjava.model.BaseEntity;
@@ -21,4 +23,11 @@ public static void assureIdConsistent(BaseEntity entity, int id) {
throw new IllegalRequestDataException(entity.getClass().getSimpleName() + " must has 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;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/web/AccountController.java b/src/main/java/ru/javaops/bootjava/web/AccountController.java
index a43eb3e..bba5c9c 100644
--- a/src/main/java/ru/javaops/bootjava/web/AccountController.java
+++ b/src/main/java/ru/javaops/bootjava/web/AccountController.java
@@ -16,11 +16,10 @@
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
-import ru.javaops.bootjava.AuthUser;
import ru.javaops.bootjava.model.Role;
import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.repository.UserRepository;
-import ru.javaops.bootjava.util.ValidationUtil;
+import ru.javaops.bootjava.util.validation.ValidationUtil;
import javax.validation.Valid;
import java.net.URI;
@@ -70,7 +69,7 @@ public void delete(@AuthenticationPrincipal AuthUser authUser) {
userRepository.deleteById(authUser.id());
}
- @PostMapping(value = "/register", consumes = MediaType.APPLICATION_JSON_VALUE)
+ @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(value = HttpStatus.CREATED)
public ResponseEntity> register(@Valid @RequestBody User user) {
log.info("register {}", user);
diff --git a/src/main/java/ru/javaops/bootjava/AuthUser.java b/src/main/java/ru/javaops/bootjava/web/AuthUser.java
similarity index 93%
rename from src/main/java/ru/javaops/bootjava/AuthUser.java
rename to src/main/java/ru/javaops/bootjava/web/AuthUser.java
index d4bf023..d31db91 100644
--- a/src/main/java/ru/javaops/bootjava/AuthUser.java
+++ b/src/main/java/ru/javaops/bootjava/web/AuthUser.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava;
+package ru.javaops.bootjava.web;
import lombok.Getter;
import lombok.ToString;
diff --git a/src/main/java/ru/javaops/bootjava/web/GlobalExceptionHandler.java b/src/main/java/ru/javaops/bootjava/web/GlobalExceptionHandler.java
new file mode 100644
index 0000000..60a28c2
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/web/GlobalExceptionHandler.java
@@ -0,0 +1,86 @@
+package ru.javaops.bootjava.web;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.error.ErrorAttributeOptions;
+import org.springframework.boot.web.servlet.error.ErrorAttributes;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.NonNull;
+import org.springframework.validation.BindException;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+import ru.javaops.bootjava.error.AppException;
+import ru.javaops.bootjava.util.validation.ValidationUtil;
+
+import javax.persistence.EntityNotFoundException;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.springframework.boot.web.error.ErrorAttributeOptions.Include.MESSAGE;
+
+@RestControllerAdvice
+@AllArgsConstructor
+@Slf4j
+public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
+ private final ErrorAttributes errorAttributes;
+
+ @ExceptionHandler(AppException.class)
+ public ResponseEntity> appException(WebRequest request, AppException ex) {
+ log.error("ApplicationException: {}", ex.getMessage());
+ return createResponseEntity(request, ex.getOptions(), null, ex.getStatus());
+ }
+
+ @ExceptionHandler(EntityNotFoundException.class)
+ public ResponseEntity> entityNotFoundException(WebRequest request, EntityNotFoundException ex) {
+ log.error("EntityNotFoundException: {}", ex.getMessage());
+ return createResponseEntity(request, ErrorAttributeOptions.of(MESSAGE), null, HttpStatus.UNPROCESSABLE_ENTITY);
+ }
+
+ @NonNull
+ @Override
+ protected ResponseEntity
""",
contact = @Contact(url = "https://javaops.ru/#contacts", name = "Grigory Kislin", email = "admin@javaops.ru")
),
diff --git a/src/main/java/ru/javaops/bootjava/config/RestAuthenticationEntryPoint.java b/src/main/java/ru/javaops/bootjava/config/RestAuthenticationEntryPoint.java
new file mode 100644
index 0000000..95f4ddc
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/config/RestAuthenticationEntryPoint.java
@@ -0,0 +1,22 @@
+package ru.javaops.bootjava.config;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AllArgsConstructor;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerExceptionResolver;
+
+@Component
+@AllArgsConstructor
+public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
+ @Qualifier("handlerExceptionResolver")
+ private final HandlerExceptionResolver resolver;
+
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
+ resolver.resolveException(request, response, null, authException);
+ }
+}
diff --git a/src/main/java/ru/javaops/bootjava/config/RestExceptionHandler.java b/src/main/java/ru/javaops/bootjava/config/RestExceptionHandler.java
new file mode 100644
index 0000000..bc0f5d2
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/config/RestExceptionHandler.java
@@ -0,0 +1,128 @@
+package ru.javaops.bootjava.config;
+
+import jakarta.persistence.EntityNotFoundException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.ValidationException;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.MessageSource;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.core.NestedExceptionUtils;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.http.ProblemDetail;
+import org.springframework.lang.NonNull;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.firewall.RequestRejectedException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.FieldError;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.ErrorResponse;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.NoHandlerFoundException;
+import ru.javaops.bootjava.error.*;
+
+import java.io.FileNotFoundException;
+import java.nio.file.AccessDeniedException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static ru.javaops.bootjava.error.ErrorType.*;
+
+@RestControllerAdvice
+@AllArgsConstructor
+@Slf4j
+public class RestExceptionHandler {
+ public static final String ERR_PFX = "ERR# ";
+
+ @Getter
+ private final MessageSource messageSource;
+
+ // https://stackoverflow.com/a/52254601/548473
+ static final Map, ErrorType> HTTP_STATUS_MAP = new LinkedHashMap<>() {
+ {
+// more specific first
+ put(NotFoundException.class, NOT_FOUND);
+ put(FileNotFoundException.class, NOT_FOUND);
+ put(NoHandlerFoundException.class, NOT_FOUND);
+ put(DataConflictException.class, DATA_CONFLICT);
+ put(IllegalRequestDataException.class, BAD_REQUEST);
+ put(AppException.class, APP_ERROR);
+ put(UnsupportedOperationException.class, APP_ERROR);
+ put(EntityNotFoundException.class, DATA_CONFLICT);
+ put(DataIntegrityViolationException.class, DATA_CONFLICT);
+ put(IllegalArgumentException.class, BAD_DATA);
+ put(BindException.class, BAD_REQUEST);
+ put(ValidationException.class, BAD_REQUEST);
+ put(HttpRequestMethodNotSupportedException.class, BAD_REQUEST);
+ put(MissingServletRequestParameterException.class, BAD_REQUEST);
+ put(RequestRejectedException.class, BAD_REQUEST);
+ put(AccessDeniedException.class, FORBIDDEN);
+ put(AuthenticationException.class, UNAUTHORIZED);
+ }
+ };
+
+ @ExceptionHandler(BindException.class)
+ ProblemDetail bindException(BindException ex, HttpServletRequest request) {
+ return processException(ex, request, Map.of("invalid_params", getErrorMap(ex.getBindingResult())));
+ }
+
+ // https://howtodoinjava.com/spring-mvc/spring-problemdetail-errorresponse/#5-adding-problemdetail-to-custom-exceptions
+ @ExceptionHandler(Exception.class)
+ ProblemDetail exception(Exception ex, HttpServletRequest request) {
+ return processException(ex, request, Map.of());
+ }
+
+ ProblemDetail processException(@NonNull Exception ex, HttpServletRequest request, Map additionalParams) {
+ String path = request.getRequestURI();
+ Class extends Exception> exClass = ex.getClass();
+ Optional optType = HTTP_STATUS_MAP.entrySet().stream()
+ .filter(
+ entry -> entry.getKey().isAssignableFrom(exClass)
+ )
+ .findAny().map(Map.Entry::getValue);
+ if (optType.isPresent()) {
+ log.error(ERR_PFX + "Exception {} at request {}", ex, path);
+ return createProblemDetail(ex, optType.get(), ex.getMessage(), additionalParams);
+ } else {
+ Throwable root = getRootCause(ex);
+ log.error(ERR_PFX + "Exception " + root + " at request " + path, root);
+ return createProblemDetail(ex, APP_ERROR, "Exception " + root.getClass().getSimpleName(), additionalParams);
+ }
+ }
+
+ private ProblemDetail createProblemDetail(Exception ex, ErrorType type, String defaultDetail, @NonNull Map additionalParams) {
+ ErrorResponse.Builder builder = ErrorResponse.builder(ex, type.status, defaultDetail);
+ ProblemDetail pd = builder.build().updateAndGetBody(messageSource, LocaleContextHolder.getLocale());
+ additionalParams.forEach(pd::setProperty);
+ return pd;
+ }
+
+ private Map getErrorMap(BindingResult result) {
+ Map invalidParams = new LinkedHashMap<>();
+ for (ObjectError error : result.getGlobalErrors()) {
+ invalidParams.put(error.getObjectName(), getErrorMessage(error));
+ }
+ for (FieldError error : result.getFieldErrors()) {
+ invalidParams.put(error.getField(), getErrorMessage(error));
+ }
+ log.warn("BindingException: {}", invalidParams);
+ return invalidParams;
+ }
+
+ private String getErrorMessage(ObjectError error) {
+ return messageSource.getMessage(error.getCode(), error.getArguments(), error.getDefaultMessage(), LocaleContextHolder.getLocale());
+ }
+
+ // https://stackoverflow.com/a/65442410/548473
+ @NonNull
+ private static Throwable getRootCause(@NonNull Throwable t) {
+ Throwable rootCause = NestedExceptionUtils.getRootCause(t);
+ return rootCause != null ? rootCause : t;
+ }
+}
diff --git a/src/main/java/ru/javaops/bootjava/config/SecurityConfig.java b/src/main/java/ru/javaops/bootjava/config/SecurityConfig.java
new file mode 100644
index 0000000..6fba44d
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/config/SecurityConfig.java
@@ -0,0 +1,71 @@
+package ru.javaops.bootjava.config;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import ru.javaops.bootjava.model.Role;
+import ru.javaops.bootjava.model.User;
+import ru.javaops.bootjava.repository.UserRepository;
+import ru.javaops.bootjava.web.AuthUser;
+
+import java.util.Optional;
+
+@Configuration
+@EnableWebSecurity
+@Slf4j
+@AllArgsConstructor
+public class SecurityConfig {
+ public static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+
+ private final UserRepository userRepository;
+ private final RestAuthenticationEntryPoint authenticationEntryPoint;
+
+ @Bean
+ PasswordEncoder passwordEncoder() {
+ return PASSWORD_ENCODER;
+ }
+
+ @Bean
+ UserDetailsService userDetailsService() {
+ return email -> {
+ log.debug("Authenticating '{}'", email);
+ Optional optionalUser = userRepository.findByEmailIgnoreCase(email);
+ return new AuthUser(optionalUser.orElseThrow(
+ () -> new UsernameNotFoundException("User '" + email + "' was not found")));
+ };
+ }
+
+ // https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter#configuring-websecurity
+ // https://stackoverflow.com/a/61147599/548473
+ @Bean
+ WebSecurityCustomizer webSecurityCustomizer() {
+ return web -> web.ignoring().requestMatchers("/", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**");
+ }
+
+ //https://stackoverflow.com/a/76538979/548473
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http.securityMatcher("/api/**").authorizeHttpRequests(authz -> authz
+ .requestMatchers("/api/admin/**").hasRole(Role.ADMIN.name())
+ .requestMatchers(HttpMethod.POST, "/api/profile").anonymous()
+ .requestMatchers("/api/**").authenticated()
+ ).httpBasic(Customizer.withDefaults())
+ .sessionManagement(smc -> smc
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ ).csrf(AbstractHttpConfigurer::disable);
+ return http.build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java b/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
deleted file mode 100644
index aae64bd..0000000
--- a/src/main/java/ru/javaops/bootjava/config/WebSecurityConfig.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package ru.javaops.bootjava.config;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpMethod;
-import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.core.userdetails.UsernameNotFoundException;
-import ru.javaops.bootjava.model.Role;
-import ru.javaops.bootjava.model.User;
-import ru.javaops.bootjava.repository.UserRepository;
-import ru.javaops.bootjava.util.JsonUtil;
-import ru.javaops.bootjava.web.AuthUser;
-
-import java.util.Optional;
-
-import static ru.javaops.bootjava.util.UserUtil.PASSWORD_ENCODER;
-
-@Configuration
-@EnableWebSecurity
-@Slf4j
-@AllArgsConstructor
-public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
-
- private final UserRepository userRepository;
-
- @Autowired
- private void setMapper(ObjectMapper objectMapper) {
- JsonUtil.setMapper(objectMapper);
- }
-
- @Bean
- @Override
- public UserDetailsService userDetailsServiceBean() throws Exception {
- return super.userDetailsServiceBean();
- }
-
- @Autowired
- public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
- auth.userDetailsService(
- email -> {
- log.debug("Authenticating '{}'", email);
- Optional optionalUser = userRepository.findByEmailIgnoreCase(email);
- return new AuthUser(optionalUser.orElseThrow(
- () -> new UsernameNotFoundException("User '" + email + "' was not found")));
- })
- .passwordEncoder(PASSWORD_ENCODER);
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.authorizeRequests()
- .antMatchers("/api/admin/**").hasRole(Role.ADMIN.name())
- .antMatchers(HttpMethod.POST, "/api/profile").anonymous()
- .antMatchers("/api/**").authenticated()
- .and().httpBasic()
- .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and().csrf().disable();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/error/AppException.java b/src/main/java/ru/javaops/bootjava/error/AppException.java
index 9d00dd4..9f11453 100644
--- a/src/main/java/ru/javaops/bootjava/error/AppException.java
+++ b/src/main/java/ru/javaops/bootjava/error/AppException.java
@@ -1,21 +1,10 @@
package ru.javaops.bootjava.error;
-import lombok.Getter;
-import org.springframework.boot.web.error.ErrorAttributeOptions;
-import org.springframework.http.HttpStatus;
-import org.springframework.web.server.ResponseStatusException;
+import org.springframework.lang.NonNull;
-@Getter
-public class AppException extends ResponseStatusException {
- private final ErrorAttributeOptions options;
+public class AppException extends RuntimeException {
- public AppException(HttpStatus status, String message, ErrorAttributeOptions options) {
- super(status, message);
- this.options = options;
- }
-
- @Override
- public String getMessage() {
- return getReason();
+ public AppException(@NonNull String message) {
+ super(message);
}
}
diff --git a/src/main/java/ru/javaops/bootjava/error/DataConflictException.java b/src/main/java/ru/javaops/bootjava/error/DataConflictException.java
new file mode 100644
index 0000000..b048af5
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/error/DataConflictException.java
@@ -0,0 +1,7 @@
+package ru.javaops.bootjava.error;
+
+public class DataConflictException extends AppException {
+ public DataConflictException(String msg) {
+ super(msg);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/error/ErrorType.java b/src/main/java/ru/javaops/bootjava/error/ErrorType.java
new file mode 100644
index 0000000..5fac6f3
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/error/ErrorType.java
@@ -0,0 +1,22 @@
+package ru.javaops.bootjava.error;
+
+import org.springframework.http.HttpStatus;
+
+public enum ErrorType {
+ APP_ERROR("Application error", HttpStatus.INTERNAL_SERVER_ERROR),
+ BAD_DATA("Wrong data", HttpStatus.UNPROCESSABLE_ENTITY),
+ BAD_REQUEST("Bad request", HttpStatus.UNPROCESSABLE_ENTITY),
+ DATA_CONFLICT("DataBase conflict", HttpStatus.CONFLICT),
+ NOT_FOUND("Resource not found", HttpStatus.NOT_FOUND),
+ AUTH_ERROR("Authorization error", HttpStatus.FORBIDDEN),
+ UNAUTHORIZED("Request unauthorized", HttpStatus.UNAUTHORIZED),
+ FORBIDDEN("Request forbidden", HttpStatus.FORBIDDEN);
+
+ ErrorType(String title, HttpStatus status) {
+ this.title = title;
+ this.status = status;
+ }
+
+ public final String title;
+ public final HttpStatus status;
+}
diff --git a/src/main/java/ru/javaops/bootjava/error/IllegalRequestDataException.java b/src/main/java/ru/javaops/bootjava/error/IllegalRequestDataException.java
index cb18581..1ca9eaf 100644
--- a/src/main/java/ru/javaops/bootjava/error/IllegalRequestDataException.java
+++ b/src/main/java/ru/javaops/bootjava/error/IllegalRequestDataException.java
@@ -1,12 +1,7 @@
package ru.javaops.bootjava.error;
-import org.springframework.boot.web.error.ErrorAttributeOptions;
-import org.springframework.http.HttpStatus;
-
-import static org.springframework.boot.web.error.ErrorAttributeOptions.Include.MESSAGE;
-
public class IllegalRequestDataException extends AppException {
public IllegalRequestDataException(String msg) {
- super(HttpStatus.UNPROCESSABLE_ENTITY, msg, ErrorAttributeOptions.of(MESSAGE));
+ super(msg);
}
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/error/NotFoundException.java b/src/main/java/ru/javaops/bootjava/error/NotFoundException.java
new file mode 100644
index 0000000..dab96dd
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/error/NotFoundException.java
@@ -0,0 +1,7 @@
+package ru.javaops.bootjava.error;
+
+public class NotFoundException extends AppException {
+ public NotFoundException(String msg) {
+ super(msg);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
index 44cb9af..9644938 100644
--- a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
+++ b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
@@ -1,15 +1,13 @@
package ru.javaops.bootjava.model;
-import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.domain.Persistable;
import org.springframework.data.util.ProxyUtils;
import org.springframework.util.Assert;
import ru.javaops.bootjava.HasId;
-import javax.persistence.*;
-
@MappedSuperclass
// https://stackoverflow.com/a/6084701/548473
@Access(AccessType.FIELD)
@@ -30,7 +28,6 @@ public int id() {
return id;
}
- @JsonIgnore
@Override
public boolean isNew() {
return id == null;
diff --git a/src/main/java/ru/javaops/bootjava/model/NamedEntity.java b/src/main/java/ru/javaops/bootjava/model/NamedEntity.java
index 68a743a..d103aac 100644
--- a/src/main/java/ru/javaops/bootjava/model/NamedEntity.java
+++ b/src/main/java/ru/javaops/bootjava/model/NamedEntity.java
@@ -1,16 +1,15 @@
package ru.javaops.bootjava.model;
+import jakarta.persistence.Column;
+import jakarta.persistence.MappedSuperclass;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import ru.javaops.bootjava.util.validation.NoHtml;
-import javax.persistence.Column;
-import javax.persistence.MappedSuperclass;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.Size;
-
@MappedSuperclass
@Getter
diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/model/User.java
index 4e402cb..558e55d 100644
--- a/src/main/java/ru/javaops/bootjava/model/User.java
+++ b/src/main/java/ru/javaops/bootjava/model/User.java
@@ -1,6 +1,11 @@
package ru.javaops.bootjava.model;
import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.persistence.*;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -11,26 +16,15 @@
import ru.javaops.bootjava.HasIdAndEmail;
import ru.javaops.bootjava.util.validation.NoHtml;
-import javax.persistence.*;
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.NotNull;
-import javax.validation.constraints.Size;
-import java.io.Serial;
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.Set;
+import java.util.*;
@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
-public class User extends NamedEntity implements HasIdAndEmail, Serializable {
- @Serial
- private static final long serialVersionUID = 1L;
+public class User extends NamedEntity implements HasIdAndEmail {
+// No session, no needs Serializable
@Column(name = "email", nullable = false, unique = true)
@Email
@@ -41,7 +35,7 @@ public class User extends NamedEntity implements HasIdAndEmail, Serializable {
@Column(name = "password", nullable = false)
@NotBlank
- @Size(max = 256)
+ @Size(max = 128)
// https://stackoverflow.com/a/12505165/548473
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
@@ -57,10 +51,10 @@ public class User extends NamedEntity implements HasIdAndEmail, Serializable {
@Enumerated(EnumType.STRING)
@CollectionTable(name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
- uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_roles"))
+ uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role"))
@Column(name = "role")
@ElementCollection(fetch = FetchType.EAGER)
- @JoinColumn(name = "user_id") //https://stackoverflow.com/a/62848296/548473
+ @JoinColumn
@OnDelete(action = OnDeleteAction.CASCADE)
private Set roles;
@@ -68,8 +62,8 @@ public User(User u) {
this(u.id, u.name, u.email, u.password, u.enabled, u.registered, u.roles);
}
- public User(Integer id, String name, String email, String password, Role role, Role... roles) {
- this(id, name, email, password, true, new Date(), EnumSet.of(role, roles));
+ public User(Integer id, String name, String email, String password, Role... roles) {
+ this(id, name, email, password, true, new Date(), Arrays.asList(roles));
}
public User(Integer id, String name, String email, String password, boolean enabled, Date registered, Collection roles) {
@@ -85,6 +79,10 @@ public void setRoles(Collection roles) {
this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles);
}
+ public boolean hasRole(Role role) {
+ return roles != null && roles.contains(role);
+ }
+
@Override
public String toString() {
return "User:" + id + '[' + email + ']';
diff --git a/src/main/java/ru/javaops/bootjava/repository/BaseRepository.java b/src/main/java/ru/javaops/bootjava/repository/BaseRepository.java
index 08ed1da..1d8a191 100644
--- a/src/main/java/ru/javaops/bootjava/repository/BaseRepository.java
+++ b/src/main/java/ru/javaops/bootjava/repository/BaseRepository.java
@@ -5,8 +5,7 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.transaction.annotation.Transactional;
-
-import static ru.javaops.bootjava.util.validation.ValidationUtil.checkModification;
+import ru.javaops.bootjava.error.NotFoundException;
// https://stackoverflow.com/questions/42781264/multiple-base-repositories-in-spring-data-jpa
@NoRepositoryBean
@@ -15,10 +14,18 @@ public interface BaseRepository extends JpaRepository {
// https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query.spel-expressions
@Transactional
@Modifying
- @Query("DELETE FROM #{#entityName} u WHERE u.id=:id")
+ @Query("DELETE FROM #{#entityName} e WHERE e.id=:id")
int delete(int id);
+ // https://stackoverflow.com/a/60695301/548473 (existed delete code 204, not existed: 404)
+ @SuppressWarnings("all") // transaction invoked
default void deleteExisted(int id) {
- checkModification(delete(id), id);
+ if (delete(id) == 0) {
+ throw new NotFoundException("Entity with id=" + id + " not found");
+ }
+ }
+
+ default T getExisted(int id) {
+ return findById(id).orElseThrow(() -> new NotFoundException("Entity with id=" + id + " not found"));
}
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
index 727497f..6008879 100644
--- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
+++ b/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
@@ -1,16 +1,27 @@
package ru.javaops.bootjava.repository;
-import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
+import ru.javaops.bootjava.error.NotFoundException;
import ru.javaops.bootjava.model.User;
import java.util.Optional;
+import static ru.javaops.bootjava.config.SecurityConfig.PASSWORD_ENCODER;
+
@Transactional(readOnly = true)
-@Tag(name = "User Controller")
public interface UserRepository extends BaseRepository {
-
@Query("SELECT u FROM User u WHERE u.email = LOWER(:email)")
Optional findByEmailIgnoreCase(String email);
+
+ @Transactional
+ default User prepareAndSave(User user) {
+ user.setPassword(PASSWORD_ENCODER.encode(user.getPassword()));
+ user.setEmail(user.getEmail().toLowerCase());
+ return save(user);
+ }
+
+ default User getExistedByEmail(String email) {
+ return findByEmailIgnoreCase(email).orElseThrow(() -> new NotFoundException("User with email=" + email + " not found"));
+ }
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/to/NamedTo.java b/src/main/java/ru/javaops/bootjava/to/NamedTo.java
index 0b2e4b1..4b5a7da 100644
--- a/src/main/java/ru/javaops/bootjava/to/NamedTo.java
+++ b/src/main/java/ru/javaops/bootjava/to/NamedTo.java
@@ -1,12 +1,11 @@
package ru.javaops.bootjava.to;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
import ru.javaops.bootjava.util.validation.NoHtml;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.Size;
-
@Data
@EqualsAndHashCode(callSuper = true)
public class NamedTo extends BaseTo {
diff --git a/src/main/java/ru/javaops/bootjava/to/UserTo.java b/src/main/java/ru/javaops/bootjava/to/UserTo.java
index eb0317f..3b970f5 100644
--- a/src/main/java/ru/javaops/bootjava/to/UserTo.java
+++ b/src/main/java/ru/javaops/bootjava/to/UserTo.java
@@ -1,14 +1,13 @@
package ru.javaops.bootjava.to;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
import lombok.EqualsAndHashCode;
import lombok.Value;
import ru.javaops.bootjava.HasIdAndEmail;
import ru.javaops.bootjava.util.validation.NoHtml;
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.Size;
-
@Value
@EqualsAndHashCode(callSuper = true)
public class UserTo extends NamedTo implements HasIdAndEmail {
diff --git a/src/main/java/ru/javaops/bootjava/util/UserUtil.java b/src/main/java/ru/javaops/bootjava/util/UsersUtil.java
similarity index 55%
rename from src/main/java/ru/javaops/bootjava/util/UserUtil.java
rename to src/main/java/ru/javaops/bootjava/util/UsersUtil.java
index 4cf471b..4986248 100644
--- a/src/main/java/ru/javaops/bootjava/util/UserUtil.java
+++ b/src/main/java/ru/javaops/bootjava/util/UsersUtil.java
@@ -1,16 +1,12 @@
package ru.javaops.bootjava.util;
import lombok.experimental.UtilityClass;
-import org.springframework.security.crypto.factory.PasswordEncoderFactories;
-import org.springframework.security.crypto.password.PasswordEncoder;
import ru.javaops.bootjava.model.Role;
import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.to.UserTo;
@UtilityClass
-public class UserUtil {
-
- public static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+public class UsersUtil {
public static User createNewFromTo(UserTo userTo) {
return new User(null, userTo.getName(), userTo.getEmail().toLowerCase(), userTo.getPassword(), Role.USER);
@@ -22,10 +18,4 @@ public static User updateFromTo(User user, UserTo userTo) {
user.setPassword(userTo.getPassword());
return user;
}
-
- public static User prepareToSave(User user) {
- user.setPassword(PASSWORD_ENCODER.encode(user.getPassword()));
- user.setEmail(user.getEmail().toLowerCase());
- return user;
- }
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/util/validation/NoHtml.java b/src/main/java/ru/javaops/bootjava/util/validation/NoHtml.java
index e2cba7e..40d3756 100644
--- a/src/main/java/ru/javaops/bootjava/util/validation/NoHtml.java
+++ b/src/main/java/ru/javaops/bootjava/util/validation/NoHtml.java
@@ -1,7 +1,8 @@
package ru.javaops.bootjava.util.validation;
-import javax.validation.Constraint;
-import javax.validation.Payload;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
diff --git a/src/main/java/ru/javaops/bootjava/util/validation/NoHtmlValidator.java b/src/main/java/ru/javaops/bootjava/util/validation/NoHtmlValidator.java
index b5d0536..6fbf5d3 100644
--- a/src/main/java/ru/javaops/bootjava/util/validation/NoHtmlValidator.java
+++ b/src/main/java/ru/javaops/bootjava/util/validation/NoHtmlValidator.java
@@ -1,11 +1,10 @@
package ru.javaops.bootjava.util.validation;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
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) {
diff --git a/src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java b/src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java
index 509b8a0..9ca3fb7 100644
--- a/src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java
+++ b/src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java
@@ -1,8 +1,6 @@
package ru.javaops.bootjava.util.validation;
import lombok.experimental.UtilityClass;
-import org.springframework.core.NestedExceptionUtils;
-import org.springframework.lang.NonNull;
import ru.javaops.bootjava.HasId;
import ru.javaops.bootjava.error.IllegalRequestDataException;
@@ -23,17 +21,4 @@ public static void assureIdConsistent(HasId bean, int id) {
throw new IllegalRequestDataException(bean.getClass().getSimpleName() + " must has id=" + id);
}
}
-
- public static void checkModification(int count, int id) {
- if (count == 0) {
- throw new IllegalRequestDataException("Entity with id=" + id + " not found");
- }
- }
-
- // https://stackoverflow.com/a/65442410/548473
- @NonNull
- public static Throwable getRootCause(@NonNull Throwable t) {
- Throwable rootCause = NestedExceptionUtils.getRootCause(t);
- return rootCause != null ? rootCause : t;
- }
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/web/AuthUser.java b/src/main/java/ru/javaops/bootjava/web/AuthUser.java
index d31db91..0a51442 100644
--- a/src/main/java/ru/javaops/bootjava/web/AuthUser.java
+++ b/src/main/java/ru/javaops/bootjava/web/AuthUser.java
@@ -1,12 +1,15 @@
package ru.javaops.bootjava.web;
import lombok.Getter;
-import lombok.ToString;
import org.springframework.lang.NonNull;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import ru.javaops.bootjava.model.Role;
import ru.javaops.bootjava.model.User;
+import static java.util.Objects.requireNonNull;
+
@Getter
-@ToString(of = "user")
public class AuthUser extends org.springframework.security.core.userdetails.User {
private final User user;
@@ -19,4 +22,33 @@ public AuthUser(@NonNull User user) {
public int id() {
return user.id();
}
+
+ public static AuthUser safeGet() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null) {
+ return null;
+ }
+ return (auth.getPrincipal() instanceof AuthUser au) ? au : null;
+ }
+
+ public static AuthUser get() {
+ return requireNonNull(safeGet(), "No authorized user found");
+ }
+
+ public static User authUser() {
+ return get().getUser();
+ }
+
+ public static int authId() {
+ return get().id();
+ }
+
+ public boolean hasRole(Role role) {
+ return user.hasRole(role);
+ }
+
+ @Override
+ public String toString() {
+ return "AuthUser:" + id() + '[' + user.getEmail() + ']';
+ }
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/web/GlobalExceptionHandler.java b/src/main/java/ru/javaops/bootjava/web/GlobalExceptionHandler.java
deleted file mode 100644
index 60a28c2..0000000
--- a/src/main/java/ru/javaops/bootjava/web/GlobalExceptionHandler.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package ru.javaops.bootjava.web;
-
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.web.error.ErrorAttributeOptions;
-import org.springframework.boot.web.servlet.error.ErrorAttributes;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.lang.NonNull;
-import org.springframework.validation.BindException;
-import org.springframework.validation.BindingResult;
-import org.springframework.web.bind.MethodArgumentNotValidException;
-import org.springframework.web.bind.annotation.ExceptionHandler;
-import org.springframework.web.bind.annotation.RestControllerAdvice;
-import org.springframework.web.context.request.WebRequest;
-import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
-import ru.javaops.bootjava.error.AppException;
-import ru.javaops.bootjava.util.validation.ValidationUtil;
-
-import javax.persistence.EntityNotFoundException;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import static org.springframework.boot.web.error.ErrorAttributeOptions.Include.MESSAGE;
-
-@RestControllerAdvice
-@AllArgsConstructor
-@Slf4j
-public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
- private final ErrorAttributes errorAttributes;
-
- @ExceptionHandler(AppException.class)
- public ResponseEntity> appException(WebRequest request, AppException ex) {
- log.error("ApplicationException: {}", ex.getMessage());
- return createResponseEntity(request, ex.getOptions(), null, ex.getStatus());
- }
-
- @ExceptionHandler(EntityNotFoundException.class)
- public ResponseEntity> entityNotFoundException(WebRequest request, EntityNotFoundException ex) {
- log.error("EntityNotFoundException: {}", ex.getMessage());
- return createResponseEntity(request, ErrorAttributeOptions.of(MESSAGE), null, HttpStatus.UNPROCESSABLE_ENTITY);
- }
-
- @NonNull
- @Override
- protected ResponseEntity handleExceptionInternal(
- @NonNull Exception ex, Object body, @NonNull HttpHeaders headers, @NonNull HttpStatus status, @NonNull WebRequest request) {
- log.error("Exception", ex);
- super.handleExceptionInternal(ex, body, headers, status, request);
- return createResponseEntity(request, ErrorAttributeOptions.of(), ValidationUtil.getRootCause(ex).getMessage(), status);
- }
-
- @NonNull
- @Override
- protected ResponseEntity handleMethodArgumentNotValid(
- MethodArgumentNotValidException ex,
- @NonNull HttpHeaders headers, @NonNull HttpStatus status, @NonNull WebRequest request) {
- return handleBindingErrors(ex.getBindingResult(), request);
- }
-
- @NonNull
- @Override
- protected ResponseEntity handleBindException(
- BindException ex, @NonNull HttpHeaders headers, @NonNull HttpStatus status, @NonNull WebRequest request) {
- return handleBindingErrors(ex.getBindingResult(), request);
- }
-
- private ResponseEntity handleBindingErrors(BindingResult result, WebRequest request) {
- String msg = result.getFieldErrors().stream()
- .map(fe -> String.format("[%s] %s", fe.getField(), fe.getDefaultMessage()))
- .collect(Collectors.joining("\n"));
- return createResponseEntity(request, ErrorAttributeOptions.defaults(), msg, HttpStatus.UNPROCESSABLE_ENTITY);
- }
-
- @SuppressWarnings("unchecked")
- private ResponseEntity createResponseEntity(WebRequest request, ErrorAttributeOptions options, String msg, HttpStatus status) {
- Map body = errorAttributes.getErrorAttributes(request, options);
- if (msg != null) {
- body.put("message", msg);
- }
- body.put("status", status.value());
- body.put("error", status.getReasonPhrase());
- return (ResponseEntity) ResponseEntity.status(status).body(body);
- }
-}
diff --git a/src/main/java/ru/javaops/bootjava/web/SecurityUtil.java b/src/main/java/ru/javaops/bootjava/web/SecurityUtil.java
deleted file mode 100644
index 36fcf86..0000000
--- a/src/main/java/ru/javaops/bootjava/web/SecurityUtil.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package ru.javaops.bootjava.web;
-
-import lombok.experimental.UtilityClass;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-import ru.javaops.bootjava.model.User;
-
-import static java.util.Objects.requireNonNull;
-
-@UtilityClass
-public class SecurityUtil {
-
- public static AuthUser safeGet() {
- Authentication auth = SecurityContextHolder.getContext().getAuthentication();
- if (auth == null) {
- return null;
- }
- Object principal = auth.getPrincipal();
- return (principal instanceof AuthUser) ? (AuthUser) principal : null;
- }
-
- public static AuthUser get() {
- return requireNonNull(safeGet(), "No authorized user found");
- }
-
- public static User authUser() {
- return get().getUser();
- }
-
- public static int authId() {
- return get().getUser().id();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/web/user/AbstractUserController.java b/src/main/java/ru/javaops/bootjava/web/user/AbstractUserController.java
index a41e626..2a417e5 100644
--- a/src/main/java/ru/javaops/bootjava/web/user/AbstractUserController.java
+++ b/src/main/java/ru/javaops/bootjava/web/user/AbstractUserController.java
@@ -1,16 +1,16 @@
package ru.javaops.bootjava.web.user;
-import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.repository.UserRepository;
-import ru.javaops.bootjava.util.UserUtil;
-@Slf4j
+import static org.slf4j.LoggerFactory.getLogger;
+
public abstract class AbstractUserController {
+ protected final Logger log = getLogger(getClass());
@Autowired
protected UserRepository repository;
@@ -23,17 +23,13 @@ protected void initBinder(WebDataBinder binder) {
binder.addValidators(emailValidator);
}
- public ResponseEntity get(int id) {
+ public User get(int id) {
log.info("get {}", id);
- return ResponseEntity.of(repository.findById(id));
+ return repository.getExisted(id);
}
public void delete(int id) {
log.info("delete {}", id);
repository.deleteExisted(id);
}
-
- protected User prepareAndSave(User user) {
- return repository.save(UserUtil.prepareToSave(user));
- }
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/web/user/AdminUserController.java b/src/main/java/ru/javaops/bootjava/web/user/AdminUserController.java
index 55bbf88..564a735 100644
--- a/src/main/java/ru/javaops/bootjava/web/user/AdminUserController.java
+++ b/src/main/java/ru/javaops/bootjava/web/user/AdminUserController.java
@@ -1,6 +1,6 @@
package ru.javaops.bootjava.web.user;
-import lombok.extern.slf4j.Slf4j;
+import jakarta.validation.Valid;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -10,7 +10,6 @@
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import ru.javaops.bootjava.model.User;
-import javax.validation.Valid;
import java.net.URI;
import java.util.List;
@@ -19,14 +18,14 @@
@RestController
@RequestMapping(value = AdminUserController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
-@Slf4j
+// TODO: cache only most requested, seldom changed data!
public class AdminUserController extends AbstractUserController {
static final String REST_URL = "/api/admin/users";
@Override
@GetMapping("/{id}")
- public ResponseEntity get(@PathVariable int id) {
+ public User get(@PathVariable int id) {
return super.get(id);
}
@@ -47,7 +46,7 @@ public List getAll() {
public ResponseEntity createWithLocation(@Valid @RequestBody User user) {
log.info("create {}", user);
checkNew(user);
- User created = prepareAndSave(user);
+ User created = repository.prepareAndSave(user);
URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
.path(REST_URL + "/{id}")
.buildAndExpand(created.getId()).toUri();
@@ -59,13 +58,13 @@ public ResponseEntity createWithLocation(@Valid @RequestBody User user) {
public void update(@Valid @RequestBody User user, @PathVariable int id) {
log.info("update {} with id={}", user, id);
assureIdConsistent(user, id);
- prepareAndSave(user);
+ repository.prepareAndSave(user);
}
@GetMapping("/by-email")
- public ResponseEntity getByEmail(@RequestParam String email) {
+ public User getByEmail(@RequestParam String email) {
log.info("getByEmail {}", email);
- return ResponseEntity.of(repository.findByEmailIgnoreCase(email));
+ return repository.getExistedByEmail(email);
}
@PatchMapping("/{id}")
@@ -73,7 +72,7 @@ public ResponseEntity getByEmail(@RequestParam String email) {
@Transactional
public void enable(@PathVariable int id, @RequestParam boolean enabled) {
log.info(enabled ? "enable {}" : "disable {}", id);
- User user = repository.getById(id);
+ User user = repository.getExisted(id);
user.setEnabled(enabled);
}
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/web/user/ProfileController.java b/src/main/java/ru/javaops/bootjava/web/user/ProfileController.java
index 278324a..e71dcd9 100644
--- a/src/main/java/ru/javaops/bootjava/web/user/ProfileController.java
+++ b/src/main/java/ru/javaops/bootjava/web/user/ProfileController.java
@@ -1,5 +1,6 @@
package ru.javaops.bootjava.web.user;
+import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
@@ -10,10 +11,9 @@
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import ru.javaops.bootjava.model.User;
import ru.javaops.bootjava.to.UserTo;
-import ru.javaops.bootjava.util.UserUtil;
+import ru.javaops.bootjava.util.UsersUtil;
import ru.javaops.bootjava.web.AuthUser;
-import javax.validation.Valid;
import java.net.URI;
import static ru.javaops.bootjava.util.validation.ValidationUtil.assureIdConsistent;
@@ -22,11 +22,13 @@
@RestController
@RequestMapping(value = ProfileController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
@Slf4j
+// TODO: cache only most requested data!
public class ProfileController extends AbstractUserController {
static final String REST_URL = "/api/profile";
@GetMapping
public User get(@AuthenticationPrincipal AuthUser authUser) {
+ log.info("get {}", authUser);
return authUser.getUser();
}
@@ -41,7 +43,7 @@ public void delete(@AuthenticationPrincipal AuthUser authUser) {
public ResponseEntity register(@Valid @RequestBody UserTo userTo) {
log.info("register {}", userTo);
checkNew(userTo);
- User created = prepareAndSave(UserUtil.createNewFromTo(userTo));
+ User created = repository.prepareAndSave(UsersUtil.createNewFromTo(userTo));
URI uriOfNewResource = ServletUriComponentsBuilder.fromCurrentContextPath()
.path(REST_URL).build().toUri();
return ResponseEntity.created(uriOfNewResource).body(created);
@@ -51,8 +53,9 @@ public ResponseEntity register(@Valid @RequestBody UserTo userTo) {
@ResponseStatus(HttpStatus.NO_CONTENT)
@Transactional
public void update(@RequestBody @Valid UserTo userTo, @AuthenticationPrincipal AuthUser authUser) {
+ log.info("update {} with id={}", userTo, authUser.id());
assureIdConsistent(userTo, authUser.id());
User user = authUser.getUser();
- prepareAndSave(UserUtil.updateFromTo(user, userTo));
+ repository.prepareAndSave(UsersUtil.updateFromTo(user, userTo));
}
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/web/user/UniqueMailValidator.java b/src/main/java/ru/javaops/bootjava/web/user/UniqueMailValidator.java
index 2d8011e..8d0ea22 100644
--- a/src/main/java/ru/javaops/bootjava/web/user/UniqueMailValidator.java
+++ b/src/main/java/ru/javaops/bootjava/web/user/UniqueMailValidator.java
@@ -1,5 +1,6 @@
package ru.javaops.bootjava.web.user;
+import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@@ -7,9 +8,7 @@
import org.springframework.validation.Errors;
import ru.javaops.bootjava.HasIdAndEmail;
import ru.javaops.bootjava.repository.UserRepository;
-import ru.javaops.bootjava.web.SecurityUtil;
-
-import javax.servlet.http.HttpServletRequest;
+import ru.javaops.bootjava.web.AuthUser;
@Component
@AllArgsConstructor
@@ -33,13 +32,13 @@ public void validate(@NonNull Object target, @NonNull Errors errors) {
if (request.getMethod().equals("PUT")) { // UPDATE
int dbId = dbUser.id();
- // it is ok, if update ourself
+ // 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 called after this validation
String requestURI = request.getRequestURI();
- if (requestURI.endsWith("/" + dbId) || (dbId == SecurityUtil.authId() && requestURI.contains("/profile")))
+ if (requestURI.endsWith("/" + dbId) || (dbId == AuthUser.authId() && requestURI.contains("/profile")))
return;
}
errors.rejectValue("email", "", EXCEPTION_DUPLICATE_EMAIL);
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 9f55124..53da502 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -51,4 +51,6 @@ server.servlet:
encoding:
charset: UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly
enabled: true # Enable http encoding support
- force: true
\ No newline at end of file
+ force: true
+
+springdoc.swagger-ui.path: /
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
index a37617f..2fd1045 100644
--- a/src/main/resources/data.sql
+++ b/src/main/resources/data.sql
@@ -1,8 +1,9 @@
-INSERT INTO USERS (NAME, EMAIL, PASSWORD)
-VALUES ('User', 'user@gmail.com', '{noop}password'),
- ('Admin', 'admin@javaops.ru', '{noop}admin');
+INSERT INTO USERS (name, email, password)
+VALUES ('User', 'user@yandex.ru', '{noop}password'),
+ ('Admin', 'admin@gmail.com', '{noop}admin'),
+ ('Guest', 'guest@gmail.com', '{noop}guest');
-INSERT INTO USER_ROLE (ROLE, USER_ID)
+INSERT INTO USER_ROLE (role, user_id)
VALUES ('USER', 1),
('ADMIN', 2),
('USER', 2);
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/user/AdminUserControllerTest.java b/src/test/java/ru/javaops/bootjava/web/user/AdminUserControllerTest.java
index 541fafc..ceb7a45 100644
--- a/src/test/java/ru/javaops/bootjava/web/user/AdminUserControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/web/user/AdminUserControllerTest.java
@@ -18,20 +18,20 @@
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.javaops.bootjava.web.user.UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL;
+import static ru.javaops.bootjava.web.user.AdminUserController.REST_URL;
import static ru.javaops.bootjava.web.user.UserTestData.*;
class AdminUserControllerTest extends AbstractControllerTest {
- private static final String REST_URL = AdminUserController.REST_URL + '/';
+ private static final String REST_URL_SLASH = REST_URL + '/';
@Autowired
- private UserRepository userRepository;
+ private UserRepository repository;
@Test
@WithUserDetails(value = ADMIN_MAIL)
void get() throws Exception {
- perform(MockMvcRequestBuilders.get(REST_URL + ADMIN_ID))
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + ADMIN_ID))
.andExpect(status().isOk())
.andDo(print())
// https://jira.spring.io/browse/SPR-14472
@@ -42,7 +42,7 @@ void get() throws Exception {
@Test
@WithUserDetails(value = ADMIN_MAIL)
void getNotFound() throws Exception {
- perform(MockMvcRequestBuilders.get(REST_URL + NOT_FOUND))
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + NOT_FOUND))
.andDo(print())
.andExpect(status().isNotFound());
}
@@ -50,7 +50,7 @@ void getNotFound() throws Exception {
@Test
@WithUserDetails(value = ADMIN_MAIL)
void getByEmail() throws Exception {
- perform(MockMvcRequestBuilders.get(REST_URL + "by-email?email=" + admin.getEmail()))
+ perform(MockMvcRequestBuilders.get(REST_URL_SLASH + "by-email?email=" + admin.getEmail()))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(USER_MATCHER.contentJson(admin));
@@ -59,33 +59,34 @@ void getByEmail() throws Exception {
@Test
@WithUserDetails(value = ADMIN_MAIL)
void delete() throws Exception {
- perform(MockMvcRequestBuilders.delete(REST_URL + USER_ID))
+ perform(MockMvcRequestBuilders.delete(REST_URL_SLASH + USER_ID))
.andDo(print())
.andExpect(status().isNoContent());
- assertFalse(userRepository.findById(USER_ID).isPresent());
+ assertFalse(repository.findById(USER_ID).isPresent());
}
@Test
@WithUserDetails(value = ADMIN_MAIL)
void deleteNotFound() throws Exception {
- perform(MockMvcRequestBuilders.delete(REST_URL + NOT_FOUND))
+ perform(MockMvcRequestBuilders.delete(REST_URL_SLASH + NOT_FOUND))
.andDo(print())
- .andExpect(status().isUnprocessableEntity());
+ .andExpect(status().isNotFound());
}
@Test
@WithUserDetails(value = ADMIN_MAIL)
void enableNotFound() throws Exception {
- perform(MockMvcRequestBuilders.patch(REST_URL + NOT_FOUND)
+ perform(MockMvcRequestBuilders.patch(REST_URL_SLASH + NOT_FOUND)
.param("enabled", "false")
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
- .andExpect(status().isUnprocessableEntity());
+ .andExpect(status().isNotFound());
}
@Test
void getUnAuth() throws Exception {
perform(MockMvcRequestBuilders.get(REST_URL))
+ .andDo(print())
.andExpect(status().isUnauthorized());
}
@@ -101,13 +102,13 @@ void getForbidden() throws Exception {
void update() throws Exception {
User updated = getUpdated();
updated.setId(null);
- perform(MockMvcRequestBuilders.put(REST_URL + USER_ID)
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + USER_ID)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonWithPassword(updated, "newPass")))
.andDo(print())
.andExpect(status().isNoContent());
- USER_MATCHER.assertMatch(userRepository.getById(USER_ID), getUpdated());
+ USER_MATCHER.assertMatch(repository.getExisted(USER_ID), getUpdated());
}
@Test
@@ -123,7 +124,7 @@ void createWithLocation() throws Exception {
int newId = created.id();
newUser.setId(newId);
USER_MATCHER.assertMatch(created, newUser);
- USER_MATCHER.assertMatch(userRepository.getById(newId), newUser);
+ USER_MATCHER.assertMatch(repository.getExisted(newId), newUser);
}
@Test
@@ -132,19 +133,19 @@ void getAll() throws Exception {
perform(MockMvcRequestBuilders.get(REST_URL))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
- .andExpect(USER_MATCHER.contentJson(admin, user));
+ .andExpect(USER_MATCHER.contentJson(admin, guest, user));
}
@Test
@WithUserDetails(value = ADMIN_MAIL)
void enable() throws Exception {
- perform(MockMvcRequestBuilders.patch(REST_URL + USER_ID)
+ perform(MockMvcRequestBuilders.patch(REST_URL_SLASH + USER_ID)
.param("enabled", "false")
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isNoContent());
- assertFalse(userRepository.getById(USER_ID).isEnabled());
+ assertFalse(repository.getExisted(USER_ID).isEnabled());
}
@Test
@@ -163,7 +164,7 @@ void createInvalid() throws Exception {
void updateInvalid() throws Exception {
User invalid = new User(user);
invalid.setName("");
- perform(MockMvcRequestBuilders.put(REST_URL + USER_ID)
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + USER_ID)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonWithPassword(invalid, "password")))
.andDo(print())
@@ -175,7 +176,7 @@ void updateInvalid() throws Exception {
void updateHtmlUnsafe() throws Exception {
User updated = new User(user);
updated.setName("");
- perform(MockMvcRequestBuilders.put(REST_URL + USER_ID)
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + USER_ID)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonWithPassword(updated, "password")))
.andDo(print())
@@ -188,12 +189,12 @@ void updateHtmlUnsafe() throws Exception {
void updateDuplicate() throws Exception {
User updated = new User(user);
updated.setEmail(ADMIN_MAIL);
- perform(MockMvcRequestBuilders.put(REST_URL + USER_ID)
+ perform(MockMvcRequestBuilders.put(REST_URL_SLASH + USER_ID)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonWithPassword(updated, "password")))
.andDo(print())
.andExpect(status().isUnprocessableEntity())
- .andExpect(content().string(containsString(EXCEPTION_DUPLICATE_EMAIL)));
+ .andExpect(content().string(containsString(UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL)));
}
@Test
@@ -206,6 +207,6 @@ void createDuplicate() throws Exception {
.content(jsonWithPassword(expected, "newPass")))
.andDo(print())
.andExpect(status().isUnprocessableEntity())
- .andExpect(content().string(containsString(EXCEPTION_DUPLICATE_EMAIL)));
+ .andExpect(content().string(containsString(UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL)));
}
}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/user/ProfileControllerTest.java b/src/test/java/ru/javaops/bootjava/web/user/ProfileControllerTest.java
index 527c298..53a7a61 100644
--- a/src/test/java/ru/javaops/bootjava/web/user/ProfileControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/web/user/ProfileControllerTest.java
@@ -10,7 +10,7 @@
import ru.javaops.bootjava.repository.UserRepository;
import ru.javaops.bootjava.to.UserTo;
import ru.javaops.bootjava.util.JsonUtil;
-import ru.javaops.bootjava.util.UserUtil;
+import ru.javaops.bootjava.util.UsersUtil;
import ru.javaops.bootjava.web.AbstractControllerTest;
import static org.hamcrest.Matchers.containsString;
@@ -18,13 +18,12 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static ru.javaops.bootjava.web.user.ProfileController.REST_URL;
-import static ru.javaops.bootjava.web.user.UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL;
import static ru.javaops.bootjava.web.user.UserTestData.*;
class ProfileControllerTest extends AbstractControllerTest {
@Autowired
- private UserRepository userRepository;
+ private UserRepository repository;
@Test
@WithUserDetails(value = USER_MAIL)
@@ -46,13 +45,13 @@ void getUnAuth() throws Exception {
void delete() throws Exception {
perform(MockMvcRequestBuilders.delete(REST_URL))
.andExpect(status().isNoContent());
- USER_MATCHER.assertMatch(userRepository.findAll(), admin);
+ USER_MATCHER.assertMatch(repository.findAll(), admin, guest);
}
@Test
void register() throws Exception {
UserTo newTo = new UserTo(null, "newName", "newemail@ya.ru", "newPassword");
- User newUser = UserUtil.createNewFromTo(newTo);
+ User newUser = UsersUtil.createNewFromTo(newTo);
ResultActions action = perform(MockMvcRequestBuilders.post(REST_URL)
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.writeValue(newTo)))
@@ -63,7 +62,7 @@ void register() throws Exception {
int newId = created.id();
newUser.setId(newId);
USER_MATCHER.assertMatch(created, newUser);
- USER_MATCHER.assertMatch(userRepository.getById(newId), newUser);
+ USER_MATCHER.assertMatch(repository.getExisted(newId), newUser);
}
@Test
@@ -75,7 +74,7 @@ void update() throws Exception {
.andDo(print())
.andExpect(status().isNoContent());
- USER_MATCHER.assertMatch(userRepository.getById(USER_ID), UserUtil.updateFromTo(new User(user), updatedTo));
+ USER_MATCHER.assertMatch(repository.getExisted(USER_ID), UsersUtil.updateFromTo(new User(user), updatedTo));
}
@Test
@@ -107,6 +106,6 @@ void updateDuplicate() throws Exception {
.content(JsonUtil.writeValue(updatedTo)))
.andDo(print())
.andExpect(status().isUnprocessableEntity())
- .andExpect(content().string(containsString(EXCEPTION_DUPLICATE_EMAIL)));
+ .andExpect(content().string(containsString(UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL)));
}
}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/user/UserTestData.java b/src/test/java/ru/javaops/bootjava/web/user/UserTestData.java
index f4841b6..a4a3af6 100644
--- a/src/test/java/ru/javaops/bootjava/web/user/UserTestData.java
+++ b/src/test/java/ru/javaops/bootjava/web/user/UserTestData.java
@@ -13,12 +13,15 @@ public class UserTestData {
public static final int USER_ID = 1;
public static final int ADMIN_ID = 2;
+ public static final int GUEST_ID = 3;
public static final int NOT_FOUND = 100;
- public static final String USER_MAIL = "user@gmail.com";
- public static final String ADMIN_MAIL = "admin@javaops.ru";
+ public static final String USER_MAIL = "user@yandex.ru";
+ public static final String ADMIN_MAIL = "admin@gmail.com";
+ public static final String GUEST_MAIL = "guest@gmail.com";
public static final User user = new User(USER_ID, "User", USER_MAIL, "password", Role.USER);
public static final User admin = new User(ADMIN_ID, "Admin", ADMIN_MAIL, "admin", Role.ADMIN, Role.USER);
+ public static final User guest = new User(GUEST_ID, "Guest", GUEST_MAIL, "guest");
public static User getNew() {
return new User(null, "New", "new@gmail.com", "newPass", false, new Date(), Collections.singleton(Role.USER));
From e0f3ae3e6167be312f126a065fdf460942561241 Mon Sep 17 00:00:00 2001
From: JavaOPs
Date: Tue, 5 Nov 2024 01:01:50 +0300
Subject: [PATCH 27/27] 8_02_update_fix_refactoring
---
pom.xml | 18 +++-
.../ru/javaops/bootjava/app/AuthUser.java | 30 ++++++
.../ru/javaops/bootjava/app/AuthUtil.java | 20 ++++
.../bootjava/{ => app}/config/AppConfig.java | 4 +-
.../{ => app}/config/OpenApiConfig.java | 2 +-
.../config/RestExceptionHandler.java | 96 +++++++++++--------
.../{ => app}/config/SecurityConfig.java | 38 +++-----
.../BaseRepository.java | 4 +-
.../javaops/bootjava/{ => common}/HasId.java | 2 +-
.../bootjava/{ => common}/HasIdAndEmail.java | 2 +-
.../bootjava/common/error/AppException.java | 14 +++
.../common/error/DataConflictException.java | 9 ++
.../{ => common}/error/ErrorType.java | 2 +-
.../error/IllegalRequestDataException.java | 9 ++
.../common/error/NotFoundException.java | 9 ++
.../bootjava/common/model/BaseEntity.java | 41 ++++++++
.../{ => common}/model/NamedEntity.java | 4 +-
.../bootjava/{ => common}/to/BaseTo.java | 4 +-
.../bootjava/{ => common}/to/NamedTo.java | 6 +-
.../common/util/HibernateProxyHelper.java | 24 +++++
.../bootjava/{ => common}/util/JsonUtil.java | 2 +-
.../{util => common}/validation/NoHtml.java | 9 +-
.../validation/NoHtmlValidator.java | 2 +-
.../validation/ValidationUtil.java | 6 +-
.../config/RestAuthenticationEntryPoint.java | 22 -----
.../javaops/bootjava/error/AppException.java | 10 --
.../bootjava/error/DataConflictException.java | 7 --
.../error/IllegalRequestDataException.java | 7 --
.../bootjava/error/NotFoundException.java | 7 --
.../ru/javaops/bootjava/model/BaseEntity.java | 58 -----------
.../bootjava/{util => user}/UsersUtil.java | 8 +-
.../bootjava/{ => user}/model/Role.java | 2 +-
.../bootjava/{ => user}/model/User.java | 23 ++---
.../{ => user}/repository/UserRepository.java | 9 +-
.../bootjava/{ => user}/to/UserTo.java | 9 +-
.../web}/AbstractUserController.java | 6 +-
.../web}/AdminUserController.java | 8 +-
.../user => user/web}/ProfileController.java | 14 +--
.../web}/UniqueMailValidator.java | 10 +-
.../ru/javaops/bootjava/web/AuthUser.java | 54 -----------
.../{web => }/AbstractControllerTest.java | 2 +-
.../bootjava/{web => }/MatcherFactory.java | 4 +-
.../bootjava/{web => }/user/UserTestData.java | 13 +--
.../web}/AdminUserControllerTest.java | 23 ++---
.../web}/ProfileControllerTest.java | 18 ++--
45 files changed, 338 insertions(+), 333 deletions(-)
create mode 100644 src/main/java/ru/javaops/bootjava/app/AuthUser.java
create mode 100644 src/main/java/ru/javaops/bootjava/app/AuthUtil.java
rename src/main/java/ru/javaops/bootjava/{ => app}/config/AppConfig.java (95%)
rename src/main/java/ru/javaops/bootjava/{ => app}/config/OpenApiConfig.java (97%)
rename src/main/java/ru/javaops/bootjava/{ => app}/config/RestExceptionHandler.java (63%)
rename src/main/java/ru/javaops/bootjava/{ => app}/config/SecurityConfig.java (60%)
rename src/main/java/ru/javaops/bootjava/{repository => common}/BaseRepository.java (92%)
rename src/main/java/ru/javaops/bootjava/{ => common}/HasId.java (91%)
rename src/main/java/ru/javaops/bootjava/{ => common}/HasIdAndEmail.java (66%)
create mode 100644 src/main/java/ru/javaops/bootjava/common/error/AppException.java
create mode 100644 src/main/java/ru/javaops/bootjava/common/error/DataConflictException.java
rename src/main/java/ru/javaops/bootjava/{ => common}/error/ErrorType.java (94%)
create mode 100644 src/main/java/ru/javaops/bootjava/common/error/IllegalRequestDataException.java
create mode 100644 src/main/java/ru/javaops/bootjava/common/error/NotFoundException.java
create mode 100644 src/main/java/ru/javaops/bootjava/common/model/BaseEntity.java
rename src/main/java/ru/javaops/bootjava/{ => common}/model/NamedEntity.java (88%)
rename src/main/java/ru/javaops/bootjava/{ => common}/to/BaseTo.java (86%)
rename src/main/java/ru/javaops/bootjava/{ => common}/to/NamedTo.java (80%)
create mode 100644 src/main/java/ru/javaops/bootjava/common/util/HibernateProxyHelper.java
rename src/main/java/ru/javaops/bootjava/{ => common}/util/JsonUtil.java (97%)
rename src/main/java/ru/javaops/bootjava/{util => common}/validation/NoHtml.java (66%)
rename src/main/java/ru/javaops/bootjava/{util => common}/validation/NoHtmlValidator.java (89%)
rename src/main/java/ru/javaops/bootjava/{util => common}/validation/ValidationUtil.java (81%)
delete mode 100644 src/main/java/ru/javaops/bootjava/config/RestAuthenticationEntryPoint.java
delete mode 100644 src/main/java/ru/javaops/bootjava/error/AppException.java
delete mode 100644 src/main/java/ru/javaops/bootjava/error/DataConflictException.java
delete mode 100644 src/main/java/ru/javaops/bootjava/error/IllegalRequestDataException.java
delete mode 100644 src/main/java/ru/javaops/bootjava/error/NotFoundException.java
delete mode 100644 src/main/java/ru/javaops/bootjava/model/BaseEntity.java
rename src/main/java/ru/javaops/bootjava/{util => user}/UsersUtil.java (75%)
rename src/main/java/ru/javaops/bootjava/{ => user}/model/Role.java (87%)
rename src/main/java/ru/javaops/bootjava/{ => user}/model/User.java (79%)
rename src/main/java/ru/javaops/bootjava/{ => user}/repository/UserRepository.java (73%)
rename src/main/java/ru/javaops/bootjava/{ => user}/to/UserTo.java (78%)
rename src/main/java/ru/javaops/bootjava/{web/user => user/web}/AbstractUserController.java (85%)
rename src/main/java/ru/javaops/bootjava/{web/user => user/web}/AdminUserController.java (91%)
rename src/main/java/ru/javaops/bootjava/{web/user => user/web}/ProfileController.java (85%)
rename src/main/java/ru/javaops/bootjava/{web/user => user/web}/UniqueMailValidator.java (87%)
delete mode 100644 src/main/java/ru/javaops/bootjava/web/AuthUser.java
rename src/test/java/ru/javaops/bootjava/{web => }/AbstractControllerTest.java (97%)
rename src/test/java/ru/javaops/bootjava/{web => }/MatcherFactory.java (97%)
rename src/test/java/ru/javaops/bootjava/{web => }/user/UserTestData.java (82%)
rename src/test/java/ru/javaops/bootjava/{web/user => user/web}/AdminUserControllerTest.java (90%)
rename src/test/java/ru/javaops/bootjava/{web/user => user/web}/ProfileControllerTest.java (90%)
diff --git a/pom.xml b/pom.xml
index b269b1e..8a475c3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.1.2
+ 3.3.3
ru.javaops
@@ -16,9 +16,9 @@
https://javaops.ru/view/bootjava
- 17
- 2.2.0
- 1.16.1
+ 21
+ 2.6.0
+ 1.18.1
UTF-8
UTF-8
@@ -78,6 +78,12 @@
lombok
true
+
+ com.google.code.findbugs
+ annotations
+ 3.0.1
+ compile
+
org.springframework.boot
@@ -108,6 +114,10 @@
org.projectlombok
lombok
+
+ com.google.code.findbugs
+ annotations
+
diff --git a/src/main/java/ru/javaops/bootjava/app/AuthUser.java b/src/main/java/ru/javaops/bootjava/app/AuthUser.java
new file mode 100644
index 0000000..51d96d5
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/app/AuthUser.java
@@ -0,0 +1,30 @@
+package ru.javaops.bootjava.app;
+
+import lombok.Getter;
+import org.springframework.lang.NonNull;
+import ru.javaops.bootjava.user.model.Role;
+import ru.javaops.bootjava.user.model.User;
+
+public class AuthUser extends org.springframework.security.core.userdetails.User {
+
+ @Getter
+ private final User user;
+
+ public AuthUser(@NonNull User user) {
+ super(user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, user.getRoles());
+ this.user = user;
+ }
+
+ public int id() {
+ return user.id();
+ }
+
+ public boolean hasRole(Role role) {
+ return user.hasRole(role);
+ }
+
+ @Override
+ public String toString() {
+ return "AuthUser:" + id() + '[' + user.getEmail() + ']';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/app/AuthUtil.java b/src/main/java/ru/javaops/bootjava/app/AuthUtil.java
new file mode 100644
index 0000000..08fcbd0
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/app/AuthUtil.java
@@ -0,0 +1,20 @@
+package ru.javaops.bootjava.app;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static java.util.Objects.requireNonNull;
+
+public class AuthUtil {
+ public static AuthUser safeGet() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null) {
+ return null;
+ }
+ return (auth.getPrincipal() instanceof AuthUser au) ? au : null;
+ }
+
+ public static AuthUser get() {
+ return requireNonNull(safeGet(), "No authorized user found");
+ }
+}
diff --git a/src/main/java/ru/javaops/bootjava/config/AppConfig.java b/src/main/java/ru/javaops/bootjava/app/config/AppConfig.java
similarity index 95%
rename from src/main/java/ru/javaops/bootjava/config/AppConfig.java
rename to src/main/java/ru/javaops/bootjava/app/config/AppConfig.java
index 44e6e52..4828ce6 100644
--- a/src/main/java/ru/javaops/bootjava/config/AppConfig.java
+++ b/src/main/java/ru/javaops/bootjava/app/config/AppConfig.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.config;
+package ru.javaops.bootjava.app.config;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@@ -12,7 +12,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ProblemDetail;
-import ru.javaops.bootjava.util.JsonUtil;
+import ru.javaops.bootjava.common.util.JsonUtil;
import java.sql.SQLException;
import java.util.Map;
diff --git a/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java b/src/main/java/ru/javaops/bootjava/app/config/OpenApiConfig.java
similarity index 97%
rename from src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java
rename to src/main/java/ru/javaops/bootjava/app/config/OpenApiConfig.java
index 546db4c..e06783e 100644
--- a/src/main/java/ru/javaops/bootjava/config/OpenApiConfig.java
+++ b/src/main/java/ru/javaops/bootjava/app/config/OpenApiConfig.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.config;
+package ru.javaops.bootjava.app.config;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
diff --git a/src/main/java/ru/javaops/bootjava/config/RestExceptionHandler.java b/src/main/java/ru/javaops/bootjava/app/config/RestExceptionHandler.java
similarity index 63%
rename from src/main/java/ru/javaops/bootjava/config/RestExceptionHandler.java
rename to src/main/java/ru/javaops/bootjava/app/config/RestExceptionHandler.java
index bc0f5d2..d383459 100644
--- a/src/main/java/ru/javaops/bootjava/config/RestExceptionHandler.java
+++ b/src/main/java/ru/javaops/bootjava/app/config/RestExceptionHandler.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.config;
+package ru.javaops.bootjava.app.config;
import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
@@ -20,19 +20,22 @@
import org.springframework.validation.ObjectError;
import org.springframework.web.ErrorResponse;
import org.springframework.web.HttpRequestMethodNotSupportedException;
-import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
-import ru.javaops.bootjava.error.*;
+import org.springframework.web.servlet.resource.NoResourceFoundException;
+import ru.javaops.bootjava.common.error.AppException;
+import ru.javaops.bootjava.common.error.ErrorType;
import java.io.FileNotFoundException;
+import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
-import static ru.javaops.bootjava.error.ErrorType.*;
+import static ru.javaops.bootjava.common.error.ErrorType.*;
@RestControllerAdvice
@AllArgsConstructor
@@ -47,76 +50,89 @@ public class RestExceptionHandler {
static final Map, ErrorType> HTTP_STATUS_MAP = new LinkedHashMap<>() {
{
// more specific first
- put(NotFoundException.class, NOT_FOUND);
+ put(NoResourceFoundException.class, NOT_FOUND);
+ put(AuthenticationException.class, UNAUTHORIZED);
put(FileNotFoundException.class, NOT_FOUND);
put(NoHandlerFoundException.class, NOT_FOUND);
- put(DataConflictException.class, DATA_CONFLICT);
- put(IllegalRequestDataException.class, BAD_REQUEST);
- put(AppException.class, APP_ERROR);
put(UnsupportedOperationException.class, APP_ERROR);
put(EntityNotFoundException.class, DATA_CONFLICT);
put(DataIntegrityViolationException.class, DATA_CONFLICT);
put(IllegalArgumentException.class, BAD_DATA);
- put(BindException.class, BAD_REQUEST);
put(ValidationException.class, BAD_REQUEST);
put(HttpRequestMethodNotSupportedException.class, BAD_REQUEST);
- put(MissingServletRequestParameterException.class, BAD_REQUEST);
+ put(ServletRequestBindingException.class, BAD_REQUEST);
put(RequestRejectedException.class, BAD_REQUEST);
put(AccessDeniedException.class, FORBIDDEN);
- put(AuthenticationException.class, UNAUTHORIZED);
}
};
@ExceptionHandler(BindException.class)
ProblemDetail bindException(BindException ex, HttpServletRequest request) {
- return processException(ex, request, Map.of("invalid_params", getErrorMap(ex.getBindingResult())));
+ Map invalidParams = getErrorMap(ex.getBindingResult());
+ String path = request.getRequestURI();
+ log.warn(ERR_PFX + "BindException with invalidParams {} at request {}", invalidParams, path);
+ return createProblemDetail(ex, path, BAD_REQUEST, "BindException", Map.of("invalid_params", invalidParams));
+ }
+
+ private Map getErrorMap(BindingResult result) {
+ Map invalidParams = new LinkedHashMap<>();
+ for (ObjectError error : result.getGlobalErrors()) {
+ invalidParams.put(error.getObjectName(), getErrorMessage(error));
+ }
+ for (FieldError error : result.getFieldErrors()) {
+ invalidParams.put(error.getField(), getErrorMessage(error));
+ }
+ return invalidParams;
+ }
+
+ private String getErrorMessage(ObjectError error) {
+ return error.getCode() == null ? error.getDefaultMessage() :
+ messageSource.getMessage(error.getCode(), error.getArguments(), error.getDefaultMessage(), LocaleContextHolder.getLocale());
}
- // https://howtodoinjava.com/spring-mvc/spring-problemdetail-errorresponse/#5-adding-problemdetail-to-custom-exceptions
@ExceptionHandler(Exception.class)
ProblemDetail exception(Exception ex, HttpServletRequest request) {
return processException(ex, request, Map.of());
}
- ProblemDetail processException(@NonNull Exception ex, HttpServletRequest request, Map additionalParams) {
+ ProblemDetail processException(@NonNull Throwable ex, HttpServletRequest request, Map additionalParams) {
+ Optional optType = findErrorType(ex);
+ if (optType.isEmpty()) {
+ Throwable root = getRootCause(ex);
+ if (root != ex) {
+ optType = findErrorType(root);
+ ex = root;
+ }
+ }
String path = request.getRequestURI();
- Class extends Exception> exClass = ex.getClass();
- Optional optType = HTTP_STATUS_MAP.entrySet().stream()
- .filter(
- entry -> entry.getKey().isAssignableFrom(exClass)
- )
- .findAny().map(Map.Entry::getValue);
if (optType.isPresent()) {
log.error(ERR_PFX + "Exception {} at request {}", ex, path);
- return createProblemDetail(ex, optType.get(), ex.getMessage(), additionalParams);
+ return createProblemDetail(ex, path, optType.get(), ex.getMessage(), additionalParams);
} else {
Throwable root = getRootCause(ex);
log.error(ERR_PFX + "Exception " + root + " at request " + path, root);
- return createProblemDetail(ex, APP_ERROR, "Exception " + root.getClass().getSimpleName(), additionalParams);
+ return createProblemDetail(ex, path, APP_ERROR, "Exception " + root.getClass().getSimpleName(), additionalParams);
}
}
- private ProblemDetail createProblemDetail(Exception ex, ErrorType type, String defaultDetail, @NonNull Map additionalParams) {
- ErrorResponse.Builder builder = ErrorResponse.builder(ex, type.status, defaultDetail);
- ProblemDetail pd = builder.build().updateAndGetBody(messageSource, LocaleContextHolder.getLocale());
- additionalParams.forEach(pd::setProperty);
- return pd;
- }
-
- private Map getErrorMap(BindingResult result) {
- Map invalidParams = new LinkedHashMap<>();
- for (ObjectError error : result.getGlobalErrors()) {
- invalidParams.put(error.getObjectName(), getErrorMessage(error));
+ private Optional findErrorType(Throwable ex) {
+ if (ex instanceof AppException aex) {
+ return Optional.of(aex.getErrorType());
}
- for (FieldError error : result.getFieldErrors()) {
- invalidParams.put(error.getField(), getErrorMessage(error));
- }
- log.warn("BindingException: {}", invalidParams);
- return invalidParams;
+ Class extends Throwable> exClass = ex.getClass();
+ return HTTP_STATUS_MAP.entrySet().stream()
+ .filter(entry -> entry.getKey().isAssignableFrom(exClass))
+ .findAny().map(Map.Entry::getValue);
}
- private String getErrorMessage(ObjectError error) {
- return messageSource.getMessage(error.getCode(), error.getArguments(), error.getDefaultMessage(), LocaleContextHolder.getLocale());
+ // https://datatracker.ietf.org/doc/html/rfc7807
+ private ProblemDetail createProblemDetail(Throwable ex, String path, ErrorType type, String defaultDetail, @NonNull Map additionalParams) {
+ ErrorResponse.Builder builder = ErrorResponse.builder(ex, type.status, defaultDetail);
+ ProblemDetail pd = builder
+ .title(type.title).instance(URI.create(path))
+ .build().updateAndGetBody(messageSource, LocaleContextHolder.getLocale());
+ additionalParams.forEach(pd::setProperty);
+ return pd;
}
// https://stackoverflow.com/a/65442410/548473
diff --git a/src/main/java/ru/javaops/bootjava/config/SecurityConfig.java b/src/main/java/ru/javaops/bootjava/app/config/SecurityConfig.java
similarity index 60%
rename from src/main/java/ru/javaops/bootjava/config/SecurityConfig.java
rename to src/main/java/ru/javaops/bootjava/app/config/SecurityConfig.java
index 6fba44d..4145004 100644
--- a/src/main/java/ru/javaops/bootjava/config/SecurityConfig.java
+++ b/src/main/java/ru/javaops/bootjava/app/config/SecurityConfig.java
@@ -1,14 +1,12 @@
-package ru.javaops.bootjava.config;
+package ru.javaops.bootjava.app.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
-import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -16,13 +14,15 @@
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
-import ru.javaops.bootjava.model.Role;
-import ru.javaops.bootjava.model.User;
-import ru.javaops.bootjava.repository.UserRepository;
-import ru.javaops.bootjava.web.AuthUser;
+import ru.javaops.bootjava.app.AuthUser;
+import ru.javaops.bootjava.user.model.Role;
+import ru.javaops.bootjava.user.model.User;
+import ru.javaops.bootjava.user.repository.UserRepository;
import java.util.Optional;
+import static org.springframework.security.config.Customizer.withDefaults;
+
@Configuration
@EnableWebSecurity
@Slf4j
@@ -31,7 +31,6 @@ public class SecurityConfig {
public static final PasswordEncoder PASSWORD_ENCODER = PasswordEncoderFactories.createDelegatingPasswordEncoder();
private final UserRepository userRepository;
- private final RestAuthenticationEntryPoint authenticationEntryPoint;
@Bean
PasswordEncoder passwordEncoder() {
@@ -48,24 +47,17 @@ UserDetailsService userDetailsService() {
};
}
- // https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter#configuring-websecurity
- // https://stackoverflow.com/a/61147599/548473
- @Bean
- WebSecurityCustomizer webSecurityCustomizer() {
- return web -> web.ignoring().requestMatchers("/", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**");
- }
-
//https://stackoverflow.com/a/76538979/548473
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
- http.securityMatcher("/api/**").authorizeHttpRequests(authz -> authz
- .requestMatchers("/api/admin/**").hasRole(Role.ADMIN.name())
- .requestMatchers(HttpMethod.POST, "/api/profile").anonymous()
- .requestMatchers("/api/**").authenticated()
- ).httpBasic(Customizer.withDefaults())
- .sessionManagement(smc -> smc
- .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- ).csrf(AbstractHttpConfigurer::disable);
+ http.securityMatcher("/api/**").authorizeHttpRequests(authz ->
+ authz.requestMatchers("/api/admin/**").hasRole(Role.ADMIN.name())
+ .requestMatchers(HttpMethod.POST, "/api/profile").anonymous()
+ .requestMatchers("/", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**").permitAll()
+ .requestMatchers("/api/**").authenticated())
+ .httpBasic(withDefaults())
+ .sessionManagement(smc -> smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/repository/BaseRepository.java b/src/main/java/ru/javaops/bootjava/common/BaseRepository.java
similarity index 92%
rename from src/main/java/ru/javaops/bootjava/repository/BaseRepository.java
rename to src/main/java/ru/javaops/bootjava/common/BaseRepository.java
index 1d8a191..edcf4ab 100644
--- a/src/main/java/ru/javaops/bootjava/repository/BaseRepository.java
+++ b/src/main/java/ru/javaops/bootjava/common/BaseRepository.java
@@ -1,11 +1,11 @@
-package ru.javaops.bootjava.repository;
+package ru.javaops.bootjava.common;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.transaction.annotation.Transactional;
-import ru.javaops.bootjava.error.NotFoundException;
+import ru.javaops.bootjava.common.error.NotFoundException;
// https://stackoverflow.com/questions/42781264/multiple-base-repositories-in-spring-data-jpa
@NoRepositoryBean
diff --git a/src/main/java/ru/javaops/bootjava/HasId.java b/src/main/java/ru/javaops/bootjava/common/HasId.java
similarity index 91%
rename from src/main/java/ru/javaops/bootjava/HasId.java
rename to src/main/java/ru/javaops/bootjava/common/HasId.java
index 42ba142..2a3ac2b 100644
--- a/src/main/java/ru/javaops/bootjava/HasId.java
+++ b/src/main/java/ru/javaops/bootjava/common/HasId.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava;
+package ru.javaops.bootjava.common;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.util.Assert;
diff --git a/src/main/java/ru/javaops/bootjava/HasIdAndEmail.java b/src/main/java/ru/javaops/bootjava/common/HasIdAndEmail.java
similarity index 66%
rename from src/main/java/ru/javaops/bootjava/HasIdAndEmail.java
rename to src/main/java/ru/javaops/bootjava/common/HasIdAndEmail.java
index aa96c88..1dd6819 100644
--- a/src/main/java/ru/javaops/bootjava/HasIdAndEmail.java
+++ b/src/main/java/ru/javaops/bootjava/common/HasIdAndEmail.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava;
+package ru.javaops.bootjava.common;
public interface HasIdAndEmail extends HasId {
String getEmail();
diff --git a/src/main/java/ru/javaops/bootjava/common/error/AppException.java b/src/main/java/ru/javaops/bootjava/common/error/AppException.java
new file mode 100644
index 0000000..8c00a78
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/common/error/AppException.java
@@ -0,0 +1,14 @@
+package ru.javaops.bootjava.common.error;
+
+import lombok.Getter;
+import org.springframework.lang.NonNull;
+
+public class AppException extends RuntimeException {
+ @Getter
+ private final ErrorType errorType;
+
+ public AppException(@NonNull String message, ErrorType errorType) {
+ super(message);
+ this.errorType = errorType;
+ }
+}
diff --git a/src/main/java/ru/javaops/bootjava/common/error/DataConflictException.java b/src/main/java/ru/javaops/bootjava/common/error/DataConflictException.java
new file mode 100644
index 0000000..fa3085a
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/common/error/DataConflictException.java
@@ -0,0 +1,9 @@
+package ru.javaops.bootjava.common.error;
+
+import static ru.javaops.bootjava.common.error.ErrorType.DATA_CONFLICT;
+
+public class DataConflictException extends AppException {
+ public DataConflictException(String msg) {
+ super(msg, DATA_CONFLICT);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/error/ErrorType.java b/src/main/java/ru/javaops/bootjava/common/error/ErrorType.java
similarity index 94%
rename from src/main/java/ru/javaops/bootjava/error/ErrorType.java
rename to src/main/java/ru/javaops/bootjava/common/error/ErrorType.java
index 5fac6f3..8d3d912 100644
--- a/src/main/java/ru/javaops/bootjava/error/ErrorType.java
+++ b/src/main/java/ru/javaops/bootjava/common/error/ErrorType.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.error;
+package ru.javaops.bootjava.common.error;
import org.springframework.http.HttpStatus;
diff --git a/src/main/java/ru/javaops/bootjava/common/error/IllegalRequestDataException.java b/src/main/java/ru/javaops/bootjava/common/error/IllegalRequestDataException.java
new file mode 100644
index 0000000..79aaec9
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/common/error/IllegalRequestDataException.java
@@ -0,0 +1,9 @@
+package ru.javaops.bootjava.common.error;
+
+import static ru.javaops.bootjava.common.error.ErrorType.BAD_REQUEST;
+
+public class IllegalRequestDataException extends AppException {
+ public IllegalRequestDataException(String msg) {
+ super(msg, BAD_REQUEST);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/common/error/NotFoundException.java b/src/main/java/ru/javaops/bootjava/common/error/NotFoundException.java
new file mode 100644
index 0000000..f1dc318
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/common/error/NotFoundException.java
@@ -0,0 +1,9 @@
+package ru.javaops.bootjava.common.error;
+
+import static ru.javaops.bootjava.common.error.ErrorType.NOT_FOUND;
+
+public class NotFoundException extends AppException {
+ public NotFoundException(String msg) {
+ super(msg, NOT_FOUND);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/common/model/BaseEntity.java b/src/main/java/ru/javaops/bootjava/common/model/BaseEntity.java
new file mode 100644
index 0000000..ae8bd2f
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/common/model/BaseEntity.java
@@ -0,0 +1,41 @@
+package ru.javaops.bootjava.common.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.persistence.*;
+import lombok.*;
+import ru.javaops.bootjava.common.HasId;
+
+import static ru.javaops.bootjava.common.util.HibernateProxyHelper.getClassWithoutInitializingProxy;
+
+@MappedSuperclass
+// https://stackoverflow.com/a/6084701/548473
+@Access(AccessType.FIELD)
+@Getter
+@Setter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+public abstract class BaseEntity implements HasId {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Schema(accessMode = Schema.AccessMode.READ_ONLY) // https://stackoverflow.com/a/28025008/548473
+ protected Integer id;
+
+ // https://stackoverflow.com/questions/1638723
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClassWithoutInitializingProxy(this) != getClassWithoutInitializingProxy(o)) return false;
+ return getId() != null && getId().equals(((BaseEntity) o).getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return getClassWithoutInitializingProxy(this).hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ":" + getId();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/model/NamedEntity.java b/src/main/java/ru/javaops/bootjava/common/model/NamedEntity.java
similarity index 88%
rename from src/main/java/ru/javaops/bootjava/model/NamedEntity.java
rename to src/main/java/ru/javaops/bootjava/common/model/NamedEntity.java
index d103aac..a4835ab 100644
--- a/src/main/java/ru/javaops/bootjava/model/NamedEntity.java
+++ b/src/main/java/ru/javaops/bootjava/common/model/NamedEntity.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.model;
+package ru.javaops.bootjava.common.model;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
@@ -8,7 +8,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-import ru.javaops.bootjava.util.validation.NoHtml;
+import ru.javaops.bootjava.common.validation.NoHtml;
@MappedSuperclass
diff --git a/src/main/java/ru/javaops/bootjava/to/BaseTo.java b/src/main/java/ru/javaops/bootjava/common/to/BaseTo.java
similarity index 86%
rename from src/main/java/ru/javaops/bootjava/to/BaseTo.java
rename to src/main/java/ru/javaops/bootjava/common/to/BaseTo.java
index 399a1cf..71e05ff 100644
--- a/src/main/java/ru/javaops/bootjava/to/BaseTo.java
+++ b/src/main/java/ru/javaops/bootjava/common/to/BaseTo.java
@@ -1,11 +1,11 @@
-package ru.javaops.bootjava.to;
+package ru.javaops.bootjava.common.to;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
-import ru.javaops.bootjava.HasId;
+import ru.javaops.bootjava.common.HasId;
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PROTECTED)
diff --git a/src/main/java/ru/javaops/bootjava/to/NamedTo.java b/src/main/java/ru/javaops/bootjava/common/to/NamedTo.java
similarity index 80%
rename from src/main/java/ru/javaops/bootjava/to/NamedTo.java
rename to src/main/java/ru/javaops/bootjava/common/to/NamedTo.java
index 4b5a7da..f2308b5 100644
--- a/src/main/java/ru/javaops/bootjava/to/NamedTo.java
+++ b/src/main/java/ru/javaops/bootjava/common/to/NamedTo.java
@@ -1,16 +1,16 @@
-package ru.javaops.bootjava.to;
+package ru.javaops.bootjava.common.to;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
-import ru.javaops.bootjava.util.validation.NoHtml;
+import ru.javaops.bootjava.common.validation.NoHtml;
@Data
@EqualsAndHashCode(callSuper = true)
public class NamedTo extends BaseTo {
@NotBlank
- @Size(min = 2, max = 128)
+ @Size(min = 2, max = 64)
@NoHtml
protected String name;
diff --git a/src/main/java/ru/javaops/bootjava/common/util/HibernateProxyHelper.java b/src/main/java/ru/javaops/bootjava/common/util/HibernateProxyHelper.java
new file mode 100644
index 0000000..b38606f
--- /dev/null
+++ b/src/main/java/ru/javaops/bootjava/common/util/HibernateProxyHelper.java
@@ -0,0 +1,24 @@
+/*
+ * Hibernate, Relational Persistence for Idiomatic Java
+ *
+ * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
+ * See the lgpl.txt file in the root directory or .
+ */
+package ru.javaops.bootjava.common.util;
+
+
+import lombok.experimental.UtilityClass;
+import org.hibernate.proxy.HibernateProxy;
+
+@UtilityClass
+public final class HibernateProxyHelper {
+
+ /**
+ * Get the class of an instance or the underlying class
+ * of a proxy (without initializing the proxy!)
+ */
+ public static Class getClassWithoutInitializingProxy(Object object) {
+ return (object instanceof HibernateProxy proxy) ?
+ proxy.getHibernateLazyInitializer().getPersistentClass() : object.getClass();
+ }
+}
diff --git a/src/main/java/ru/javaops/bootjava/util/JsonUtil.java b/src/main/java/ru/javaops/bootjava/common/util/JsonUtil.java
similarity index 97%
rename from src/main/java/ru/javaops/bootjava/util/JsonUtil.java
rename to src/main/java/ru/javaops/bootjava/common/util/JsonUtil.java
index 9c37f33..d47fbee 100644
--- a/src/main/java/ru/javaops/bootjava/util/JsonUtil.java
+++ b/src/main/java/ru/javaops/bootjava/common/util/JsonUtil.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.util;
+package ru.javaops.bootjava.common.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
diff --git a/src/main/java/ru/javaops/bootjava/util/validation/NoHtml.java b/src/main/java/ru/javaops/bootjava/common/validation/NoHtml.java
similarity index 66%
rename from src/main/java/ru/javaops/bootjava/util/validation/NoHtml.java
rename to src/main/java/ru/javaops/bootjava/common/validation/NoHtml.java
index 40d3756..f730209 100644
--- a/src/main/java/ru/javaops/bootjava/util/validation/NoHtml.java
+++ b/src/main/java/ru/javaops/bootjava/common/validation/NoHtml.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.util.validation;
+package ru.javaops.bootjava.common.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@@ -7,16 +7,15 @@
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.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = NoHtmlValidator.class)
-@Target({METHOD, FIELD})
+@Target({METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE})
@Retention(RUNTIME)
public @interface NoHtml {
- String message() default "{error.noHtml}";
+ String message() default "HTML tags forbidden";
Class>[] groups() default {};
diff --git a/src/main/java/ru/javaops/bootjava/util/validation/NoHtmlValidator.java b/src/main/java/ru/javaops/bootjava/common/validation/NoHtmlValidator.java
similarity index 89%
rename from src/main/java/ru/javaops/bootjava/util/validation/NoHtmlValidator.java
rename to src/main/java/ru/javaops/bootjava/common/validation/NoHtmlValidator.java
index 6fbf5d3..68dd323 100644
--- a/src/main/java/ru/javaops/bootjava/util/validation/NoHtmlValidator.java
+++ b/src/main/java/ru/javaops/bootjava/common/validation/NoHtmlValidator.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.util.validation;
+package ru.javaops.bootjava.common.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
diff --git a/src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java b/src/main/java/ru/javaops/bootjava/common/validation/ValidationUtil.java
similarity index 81%
rename from src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java
rename to src/main/java/ru/javaops/bootjava/common/validation/ValidationUtil.java
index 9ca3fb7..d55e887 100644
--- a/src/main/java/ru/javaops/bootjava/util/validation/ValidationUtil.java
+++ b/src/main/java/ru/javaops/bootjava/common/validation/ValidationUtil.java
@@ -1,8 +1,8 @@
-package ru.javaops.bootjava.util.validation;
+package ru.javaops.bootjava.common.validation;
import lombok.experimental.UtilityClass;
-import ru.javaops.bootjava.HasId;
-import ru.javaops.bootjava.error.IllegalRequestDataException;
+import ru.javaops.bootjava.common.HasId;
+import ru.javaops.bootjava.common.error.IllegalRequestDataException;
@UtilityClass
public class ValidationUtil {
diff --git a/src/main/java/ru/javaops/bootjava/config/RestAuthenticationEntryPoint.java b/src/main/java/ru/javaops/bootjava/config/RestAuthenticationEntryPoint.java
deleted file mode 100644
index 95f4ddc..0000000
--- a/src/main/java/ru/javaops/bootjava/config/RestAuthenticationEntryPoint.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package ru.javaops.bootjava.config;
-
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import lombok.AllArgsConstructor;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.security.core.AuthenticationException;
-import org.springframework.security.web.AuthenticationEntryPoint;
-import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.HandlerExceptionResolver;
-
-@Component
-@AllArgsConstructor
-public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
- @Qualifier("handlerExceptionResolver")
- private final HandlerExceptionResolver resolver;
-
- @Override
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
- resolver.resolveException(request, response, null, authException);
- }
-}
diff --git a/src/main/java/ru/javaops/bootjava/error/AppException.java b/src/main/java/ru/javaops/bootjava/error/AppException.java
deleted file mode 100644
index 9f11453..0000000
--- a/src/main/java/ru/javaops/bootjava/error/AppException.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package ru.javaops.bootjava.error;
-
-import org.springframework.lang.NonNull;
-
-public class AppException extends RuntimeException {
-
- public AppException(@NonNull String message) {
- super(message);
- }
-}
diff --git a/src/main/java/ru/javaops/bootjava/error/DataConflictException.java b/src/main/java/ru/javaops/bootjava/error/DataConflictException.java
deleted file mode 100644
index b048af5..0000000
--- a/src/main/java/ru/javaops/bootjava/error/DataConflictException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package ru.javaops.bootjava.error;
-
-public class DataConflictException extends AppException {
- public DataConflictException(String msg) {
- super(msg);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/error/IllegalRequestDataException.java b/src/main/java/ru/javaops/bootjava/error/IllegalRequestDataException.java
deleted file mode 100644
index 1ca9eaf..0000000
--- a/src/main/java/ru/javaops/bootjava/error/IllegalRequestDataException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package ru.javaops.bootjava.error;
-
-public class IllegalRequestDataException extends AppException {
- public IllegalRequestDataException(String msg) {
- super(msg);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/error/NotFoundException.java b/src/main/java/ru/javaops/bootjava/error/NotFoundException.java
deleted file mode 100644
index dab96dd..0000000
--- a/src/main/java/ru/javaops/bootjava/error/NotFoundException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package ru.javaops.bootjava.error;
-
-public class NotFoundException extends AppException {
- public NotFoundException(String msg) {
- super(msg);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java b/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
deleted file mode 100644
index 9644938..0000000
--- a/src/main/java/ru/javaops/bootjava/model/BaseEntity.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package ru.javaops.bootjava.model;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.persistence.*;
-import lombok.*;
-import org.springframework.data.domain.Persistable;
-import org.springframework.data.util.ProxyUtils;
-import org.springframework.util.Assert;
-import ru.javaops.bootjava.HasId;
-
-@MappedSuperclass
-// https://stackoverflow.com/a/6084701/548473
-@Access(AccessType.FIELD)
-@Getter
-@Setter
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
-@AllArgsConstructor(access = AccessLevel.PROTECTED)
-public abstract class BaseEntity implements Persistable, HasId {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- @Schema(accessMode = Schema.AccessMode.READ_ONLY) // https://stackoverflow.com/a/28025008/548473
- protected Integer 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 id == null;
- }
-
- // https://stackoverflow.com/questions/1638723
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || !getClass().equals(ProxyUtils.getUserClass(o))) {
- return false;
- }
- BaseEntity that = (BaseEntity) o;
- return id != null && id.equals(that.id);
- }
-
- @Override
- public int hashCode() {
- return id == null ? 0 : id;
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + ":" + id;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/bootjava/util/UsersUtil.java b/src/main/java/ru/javaops/bootjava/user/UsersUtil.java
similarity index 75%
rename from src/main/java/ru/javaops/bootjava/util/UsersUtil.java
rename to src/main/java/ru/javaops/bootjava/user/UsersUtil.java
index 4986248..63f3bcf 100644
--- a/src/main/java/ru/javaops/bootjava/util/UsersUtil.java
+++ b/src/main/java/ru/javaops/bootjava/user/UsersUtil.java
@@ -1,9 +1,9 @@
-package ru.javaops.bootjava.util;
+package ru.javaops.bootjava.user;
import lombok.experimental.UtilityClass;
-import ru.javaops.bootjava.model.Role;
-import ru.javaops.bootjava.model.User;
-import ru.javaops.bootjava.to.UserTo;
+import ru.javaops.bootjava.user.model.Role;
+import ru.javaops.bootjava.user.model.User;
+import ru.javaops.bootjava.user.to.UserTo;
@UtilityClass
public class UsersUtil {
diff --git a/src/main/java/ru/javaops/bootjava/model/Role.java b/src/main/java/ru/javaops/bootjava/user/model/Role.java
similarity index 87%
rename from src/main/java/ru/javaops/bootjava/model/Role.java
rename to src/main/java/ru/javaops/bootjava/user/model/Role.java
index 08bc76d..53c9c4e 100644
--- a/src/main/java/ru/javaops/bootjava/model/Role.java
+++ b/src/main/java/ru/javaops/bootjava/user/model/Role.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.model;
+package ru.javaops.bootjava.user.model;
import org.springframework.security.core.GrantedAuthority;
diff --git a/src/main/java/ru/javaops/bootjava/model/User.java b/src/main/java/ru/javaops/bootjava/user/model/User.java
similarity index 79%
rename from src/main/java/ru/javaops/bootjava/model/User.java
rename to src/main/java/ru/javaops/bootjava/user/model/User.java
index 558e55d..7dc34a0 100644
--- a/src/main/java/ru/javaops/bootjava/model/User.java
+++ b/src/main/java/ru/javaops/bootjava/user/model/User.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.model;
+package ru.javaops.bootjava.user.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
@@ -10,11 +10,10 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-import org.hibernate.annotations.OnDelete;
-import org.hibernate.annotations.OnDeleteAction;
-import org.springframework.util.CollectionUtils;
-import ru.javaops.bootjava.HasIdAndEmail;
-import ru.javaops.bootjava.util.validation.NoHtml;
+import org.springframework.lang.NonNull;
+import ru.javaops.bootjava.common.HasIdAndEmail;
+import ru.javaops.bootjava.common.model.NamedEntity;
+import ru.javaops.bootjava.common.validation.NoHtml;
import java.util.*;
@@ -29,7 +28,7 @@ public class User extends NamedEntity implements HasIdAndEmail {
@Column(name = "email", nullable = false, unique = true)
@Email
@NotBlank
- @Size(max = 128)
+ @Size(max = 64)
@NoHtml // https://stackoverflow.com/questions/17480809
private String email;
@@ -54,9 +53,7 @@ public class User extends NamedEntity implements HasIdAndEmail {
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role"}, name = "uk_user_role"))
@Column(name = "role")
@ElementCollection(fetch = FetchType.EAGER)
- @JoinColumn
- @OnDelete(action = OnDeleteAction.CASCADE)
- private Set roles;
+ private Set roles = EnumSet.noneOf(Role.class);
public User(User u) {
this(u.id, u.name, u.email, u.password, u.enabled, u.registered, u.roles);
@@ -66,7 +63,7 @@ public User(Integer id, String name, String email, String password, Role... role
this(id, name, email, password, true, new Date(), Arrays.asList(roles));
}
- public User(Integer id, String name, String email, String password, boolean enabled, Date registered, Collection roles) {
+ public User(Integer id, String name, String email, String password, boolean enabled, Date registered, @NonNull Collection roles) {
super(id, name);
this.email = email;
this.password = password;
@@ -76,11 +73,11 @@ public User(Integer id, String name, String email, String password, boolean enab
}
public void setRoles(Collection roles) {
- this.roles = CollectionUtils.isEmpty(roles) ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles);
+ this.roles = roles.isEmpty() ? EnumSet.noneOf(Role.class) : EnumSet.copyOf(roles);
}
public boolean hasRole(Role role) {
- return roles != null && roles.contains(role);
+ return roles.contains(role);
}
@Override
diff --git a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java b/src/main/java/ru/javaops/bootjava/user/repository/UserRepository.java
similarity index 73%
rename from src/main/java/ru/javaops/bootjava/repository/UserRepository.java
rename to src/main/java/ru/javaops/bootjava/user/repository/UserRepository.java
index 6008879..def57ea 100644
--- a/src/main/java/ru/javaops/bootjava/repository/UserRepository.java
+++ b/src/main/java/ru/javaops/bootjava/user/repository/UserRepository.java
@@ -1,13 +1,14 @@
-package ru.javaops.bootjava.repository;
+package ru.javaops.bootjava.user.repository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
-import ru.javaops.bootjava.error.NotFoundException;
-import ru.javaops.bootjava.model.User;
+import ru.javaops.bootjava.common.BaseRepository;
+import ru.javaops.bootjava.common.error.NotFoundException;
+import ru.javaops.bootjava.user.model.User;
import java.util.Optional;
-import static ru.javaops.bootjava.config.SecurityConfig.PASSWORD_ENCODER;
+import static ru.javaops.bootjava.app.config.SecurityConfig.PASSWORD_ENCODER;
@Transactional(readOnly = true)
public interface UserRepository extends BaseRepository {
diff --git a/src/main/java/ru/javaops/bootjava/to/UserTo.java b/src/main/java/ru/javaops/bootjava/user/to/UserTo.java
similarity index 78%
rename from src/main/java/ru/javaops/bootjava/to/UserTo.java
rename to src/main/java/ru/javaops/bootjava/user/to/UserTo.java
index 3b970f5..33922a4 100644
--- a/src/main/java/ru/javaops/bootjava/to/UserTo.java
+++ b/src/main/java/ru/javaops/bootjava/user/to/UserTo.java
@@ -1,19 +1,20 @@
-package ru.javaops.bootjava.to;
+package ru.javaops.bootjava.user.to;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.EqualsAndHashCode;
import lombok.Value;
-import ru.javaops.bootjava.HasIdAndEmail;
-import ru.javaops.bootjava.util.validation.NoHtml;
+import ru.javaops.bootjava.common.HasIdAndEmail;
+import ru.javaops.bootjava.common.to.NamedTo;
+import ru.javaops.bootjava.common.validation.NoHtml;
@Value
@EqualsAndHashCode(callSuper = true)
public class UserTo extends NamedTo implements HasIdAndEmail {
@Email
@NotBlank
- @Size(max = 128)
+ @Size(max = 64)
@NoHtml // https://stackoverflow.com/questions/17480809
String email;
diff --git a/src/main/java/ru/javaops/bootjava/web/user/AbstractUserController.java b/src/main/java/ru/javaops/bootjava/user/web/AbstractUserController.java
similarity index 85%
rename from src/main/java/ru/javaops/bootjava/web/user/AbstractUserController.java
rename to src/main/java/ru/javaops/bootjava/user/web/AbstractUserController.java
index 2a417e5..77e5aa6 100644
--- a/src/main/java/ru/javaops/bootjava/web/user/AbstractUserController.java
+++ b/src/main/java/ru/javaops/bootjava/user/web/AbstractUserController.java
@@ -1,11 +1,11 @@
-package ru.javaops.bootjava.web.user;
+package ru.javaops.bootjava.user.web;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
-import ru.javaops.bootjava.model.User;
-import ru.javaops.bootjava.repository.UserRepository;
+import ru.javaops.bootjava.user.model.User;
+import ru.javaops.bootjava.user.repository.UserRepository;
import static org.slf4j.LoggerFactory.getLogger;
diff --git a/src/main/java/ru/javaops/bootjava/web/user/AdminUserController.java b/src/main/java/ru/javaops/bootjava/user/web/AdminUserController.java
similarity index 91%
rename from src/main/java/ru/javaops/bootjava/web/user/AdminUserController.java
rename to src/main/java/ru/javaops/bootjava/user/web/AdminUserController.java
index 564a735..d310014 100644
--- a/src/main/java/ru/javaops/bootjava/web/user/AdminUserController.java
+++ b/src/main/java/ru/javaops/bootjava/user/web/AdminUserController.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.web.user;
+package ru.javaops.bootjava.user.web;
import jakarta.validation.Valid;
import org.springframework.data.domain.Sort;
@@ -8,13 +8,13 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
-import ru.javaops.bootjava.model.User;
+import ru.javaops.bootjava.user.model.User;
import java.net.URI;
import java.util.List;
-import static ru.javaops.bootjava.util.validation.ValidationUtil.assureIdConsistent;
-import static ru.javaops.bootjava.util.validation.ValidationUtil.checkNew;
+import static ru.javaops.bootjava.common.validation.ValidationUtil.assureIdConsistent;
+import static ru.javaops.bootjava.common.validation.ValidationUtil.checkNew;
@RestController
@RequestMapping(value = AdminUserController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
diff --git a/src/main/java/ru/javaops/bootjava/web/user/ProfileController.java b/src/main/java/ru/javaops/bootjava/user/web/ProfileController.java
similarity index 85%
rename from src/main/java/ru/javaops/bootjava/web/user/ProfileController.java
rename to src/main/java/ru/javaops/bootjava/user/web/ProfileController.java
index e71dcd9..96a06b6 100644
--- a/src/main/java/ru/javaops/bootjava/web/user/ProfileController.java
+++ b/src/main/java/ru/javaops/bootjava/user/web/ProfileController.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.web.user;
+package ru.javaops.bootjava.user.web;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
@@ -9,15 +9,15 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
-import ru.javaops.bootjava.model.User;
-import ru.javaops.bootjava.to.UserTo;
-import ru.javaops.bootjava.util.UsersUtil;
-import ru.javaops.bootjava.web.AuthUser;
+import ru.javaops.bootjava.app.AuthUser;
+import ru.javaops.bootjava.user.UsersUtil;
+import ru.javaops.bootjava.user.model.User;
+import ru.javaops.bootjava.user.to.UserTo;
import java.net.URI;
-import static ru.javaops.bootjava.util.validation.ValidationUtil.assureIdConsistent;
-import static ru.javaops.bootjava.util.validation.ValidationUtil.checkNew;
+import static ru.javaops.bootjava.common.validation.ValidationUtil.assureIdConsistent;
+import static ru.javaops.bootjava.common.validation.ValidationUtil.checkNew;
@RestController
@RequestMapping(value = ProfileController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE)
diff --git a/src/main/java/ru/javaops/bootjava/web/user/UniqueMailValidator.java b/src/main/java/ru/javaops/bootjava/user/web/UniqueMailValidator.java
similarity index 87%
rename from src/main/java/ru/javaops/bootjava/web/user/UniqueMailValidator.java
rename to src/main/java/ru/javaops/bootjava/user/web/UniqueMailValidator.java
index 8d0ea22..78bd024 100644
--- a/src/main/java/ru/javaops/bootjava/web/user/UniqueMailValidator.java
+++ b/src/main/java/ru/javaops/bootjava/user/web/UniqueMailValidator.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.web.user;
+package ru.javaops.bootjava.user.web;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
@@ -6,9 +6,9 @@
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
-import ru.javaops.bootjava.HasIdAndEmail;
-import ru.javaops.bootjava.repository.UserRepository;
-import ru.javaops.bootjava.web.AuthUser;
+import ru.javaops.bootjava.app.AuthUtil;
+import ru.javaops.bootjava.common.HasIdAndEmail;
+import ru.javaops.bootjava.user.repository.UserRepository;
@Component
@AllArgsConstructor
@@ -38,7 +38,7 @@ public void validate(@NonNull Object target, @NonNull Errors errors) {
// Workaround for update with user.id=null in request body
// ValidationUtil.assureIdConsistent called after this validation
String requestURI = request.getRequestURI();
- if (requestURI.endsWith("/" + dbId) || (dbId == AuthUser.authId() && requestURI.contains("/profile")))
+ if (requestURI.endsWith("/" + dbId) || (dbId == AuthUtil.get().id() && requestURI.contains("/profile")))
return;
}
errors.rejectValue("email", "", EXCEPTION_DUPLICATE_EMAIL);
diff --git a/src/main/java/ru/javaops/bootjava/web/AuthUser.java b/src/main/java/ru/javaops/bootjava/web/AuthUser.java
deleted file mode 100644
index 0a51442..0000000
--- a/src/main/java/ru/javaops/bootjava/web/AuthUser.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package ru.javaops.bootjava.web;
-
-import lombok.Getter;
-import org.springframework.lang.NonNull;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.context.SecurityContextHolder;
-import ru.javaops.bootjava.model.Role;
-import ru.javaops.bootjava.model.User;
-
-import static java.util.Objects.requireNonNull;
-
-@Getter
-public class AuthUser extends org.springframework.security.core.userdetails.User {
-
- private final User user;
-
- public AuthUser(@NonNull User user) {
- super(user.getEmail(), user.getPassword(), user.getRoles());
- this.user = user;
- }
-
- public int id() {
- return user.id();
- }
-
- public static AuthUser safeGet() {
- Authentication auth = SecurityContextHolder.getContext().getAuthentication();
- if (auth == null) {
- return null;
- }
- return (auth.getPrincipal() instanceof AuthUser au) ? au : null;
- }
-
- public static AuthUser get() {
- return requireNonNull(safeGet(), "No authorized user found");
- }
-
- public static User authUser() {
- return get().getUser();
- }
-
- public static int authId() {
- return get().id();
- }
-
- public boolean hasRole(Role role) {
- return user.hasRole(role);
- }
-
- @Override
- public String toString() {
- return "AuthUser:" + id() + '[' + user.getEmail() + ']';
- }
-}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java b/src/test/java/ru/javaops/bootjava/AbstractControllerTest.java
similarity index 97%
rename from src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java
rename to src/test/java/ru/javaops/bootjava/AbstractControllerTest.java
index b4d9cec..7320912 100644
--- a/src/test/java/ru/javaops/bootjava/web/AbstractControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/AbstractControllerTest.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.web;
+package ru.javaops.bootjava;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
diff --git a/src/test/java/ru/javaops/bootjava/web/MatcherFactory.java b/src/test/java/ru/javaops/bootjava/MatcherFactory.java
similarity index 97%
rename from src/test/java/ru/javaops/bootjava/web/MatcherFactory.java
rename to src/test/java/ru/javaops/bootjava/MatcherFactory.java
index c510818..6a41537 100644
--- a/src/test/java/ru/javaops/bootjava/web/MatcherFactory.java
+++ b/src/test/java/ru/javaops/bootjava/MatcherFactory.java
@@ -1,9 +1,9 @@
-package ru.javaops.bootjava.web;
+package ru.javaops.bootjava;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher;
-import ru.javaops.bootjava.util.JsonUtil;
+import ru.javaops.bootjava.common.util.JsonUtil;
import java.io.UnsupportedEncodingException;
import java.util.List;
diff --git a/src/test/java/ru/javaops/bootjava/web/user/UserTestData.java b/src/test/java/ru/javaops/bootjava/user/UserTestData.java
similarity index 82%
rename from src/test/java/ru/javaops/bootjava/web/user/UserTestData.java
rename to src/test/java/ru/javaops/bootjava/user/UserTestData.java
index a4a3af6..a507385 100644
--- a/src/test/java/ru/javaops/bootjava/web/user/UserTestData.java
+++ b/src/test/java/ru/javaops/bootjava/user/UserTestData.java
@@ -1,12 +1,13 @@
-package ru.javaops.bootjava.web.user;
+package ru.javaops.bootjava.user;
-import ru.javaops.bootjava.model.Role;
-import ru.javaops.bootjava.model.User;
-import ru.javaops.bootjava.util.JsonUtil;
-import ru.javaops.bootjava.web.MatcherFactory;
+import ru.javaops.bootjava.MatcherFactory;
+import ru.javaops.bootjava.common.util.JsonUtil;
+import ru.javaops.bootjava.user.model.Role;
+import ru.javaops.bootjava.user.model.User;
import java.util.Collections;
import java.util.Date;
+import java.util.List;
public class UserTestData {
public static final MatcherFactory.Matcher USER_MATCHER = MatcherFactory.usingIgnoringFieldsComparator(User.class, "registered", "password");
@@ -28,7 +29,7 @@ public static User getNew() {
}
public static User getUpdated() {
- return new User(USER_ID, "UpdatedName", USER_MAIL, "newPass", false, new Date(), Collections.singleton(Role.ADMIN));
+ return new User(USER_ID, "UpdatedName", USER_MAIL, "newPass", false, new Date(), List.of(Role.ADMIN));
}
public static String jsonWithPassword(User user, String passw) {
diff --git a/src/test/java/ru/javaops/bootjava/web/user/AdminUserControllerTest.java b/src/test/java/ru/javaops/bootjava/user/web/AdminUserControllerTest.java
similarity index 90%
rename from src/test/java/ru/javaops/bootjava/web/user/AdminUserControllerTest.java
rename to src/test/java/ru/javaops/bootjava/user/web/AdminUserControllerTest.java
index ceb7a45..10489f2 100644
--- a/src/test/java/ru/javaops/bootjava/web/user/AdminUserControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/user/web/AdminUserControllerTest.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.web.user;
+package ru.javaops.bootjava.user.web;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -6,20 +6,19 @@
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
-import org.springframework.transaction.annotation.Propagation;
-import org.springframework.transaction.annotation.Transactional;
-import ru.javaops.bootjava.model.Role;
-import ru.javaops.bootjava.model.User;
-import ru.javaops.bootjava.repository.UserRepository;
-import ru.javaops.bootjava.web.AbstractControllerTest;
+import ru.javaops.bootjava.AbstractControllerTest;
+import ru.javaops.bootjava.user.model.Role;
+import ru.javaops.bootjava.user.model.User;
+import ru.javaops.bootjava.user.repository.UserRepository;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertFalse;
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.javaops.bootjava.web.user.AdminUserController.REST_URL;
-import static ru.javaops.bootjava.web.user.UserTestData.*;
+import static ru.javaops.bootjava.user.UserTestData.*;
+import static ru.javaops.bootjava.user.web.AdminUserController.REST_URL;
+import static ru.javaops.bootjava.user.web.UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL;
class AdminUserControllerTest extends AbstractControllerTest {
@@ -184,7 +183,6 @@ void updateHtmlUnsafe() throws Exception {
}
@Test
- @Transactional(propagation = Propagation.NEVER)
@WithUserDetails(value = ADMIN_MAIL)
void updateDuplicate() throws Exception {
User updated = new User(user);
@@ -194,11 +192,10 @@ void updateDuplicate() throws Exception {
.content(jsonWithPassword(updated, "password")))
.andDo(print())
.andExpect(status().isUnprocessableEntity())
- .andExpect(content().string(containsString(UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL)));
+ .andExpect(content().string(containsString(EXCEPTION_DUPLICATE_EMAIL)));
}
@Test
- @Transactional(propagation = Propagation.NEVER)
@WithUserDetails(value = ADMIN_MAIL)
void createDuplicate() throws Exception {
User expected = new User(null, "New", USER_MAIL, "newPass", Role.USER, Role.ADMIN);
@@ -207,6 +204,6 @@ void createDuplicate() throws Exception {
.content(jsonWithPassword(expected, "newPass")))
.andDo(print())
.andExpect(status().isUnprocessableEntity())
- .andExpect(content().string(containsString(UniqueMailValidator.EXCEPTION_DUPLICATE_EMAIL)));
+ .andExpect(content().string(containsString(EXCEPTION_DUPLICATE_EMAIL)));
}
}
\ No newline at end of file
diff --git a/src/test/java/ru/javaops/bootjava/web/user/ProfileControllerTest.java b/src/test/java/ru/javaops/bootjava/user/web/ProfileControllerTest.java
similarity index 90%
rename from src/test/java/ru/javaops/bootjava/web/user/ProfileControllerTest.java
rename to src/test/java/ru/javaops/bootjava/user/web/ProfileControllerTest.java
index 53a7a61..7cc6614 100644
--- a/src/test/java/ru/javaops/bootjava/web/user/ProfileControllerTest.java
+++ b/src/test/java/ru/javaops/bootjava/user/web/ProfileControllerTest.java
@@ -1,4 +1,4 @@
-package ru.javaops.bootjava.web.user;
+package ru.javaops.bootjava.user.web;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -6,19 +6,19 @@
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
-import ru.javaops.bootjava.model.User;
-import ru.javaops.bootjava.repository.UserRepository;
-import ru.javaops.bootjava.to.UserTo;
-import ru.javaops.bootjava.util.JsonUtil;
-import ru.javaops.bootjava.util.UsersUtil;
-import ru.javaops.bootjava.web.AbstractControllerTest;
+import ru.javaops.bootjava.AbstractControllerTest;
+import ru.javaops.bootjava.common.util.JsonUtil;
+import ru.javaops.bootjava.user.UsersUtil;
+import ru.javaops.bootjava.user.model.User;
+import ru.javaops.bootjava.user.repository.UserRepository;
+import ru.javaops.bootjava.user.to.UserTo;
import static org.hamcrest.Matchers.containsString;
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.javaops.bootjava.web.user.ProfileController.REST_URL;
-import static ru.javaops.bootjava.web.user.UserTestData.*;
+import static ru.javaops.bootjava.user.UserTestData.*;
+import static ru.javaops.bootjava.user.web.ProfileController.REST_URL;
class ProfileControllerTest extends AbstractControllerTest {