From 173aa6b459489321906f85dfd190afb2bc40bcec Mon Sep 17 00:00:00 2001 From: qc80ja Date: Wed, 28 Nov 2018 15:19:47 +0100 Subject: [PATCH 01/18] ADD: In-memory H2 database ADD: Two different types of repositories to interact with the database --- .gitignore | 2 + pom.xml | 28 ++++++++- .../spring/configuration/DatabaseConfig.java | 62 +++++++++++++++++++ .../spring/hibernate/HibernateHouse.java | 35 +++++++++++ .../spring/hibernate/HibernateRepository.java | 29 +++++++++ .../java/stenden/spring/jdbc/JdbcHouse.java | 22 +++++++ .../stenden/spring/jdbc/JdbcRepository.java | 35 +++++++++++ .../spring/resource/HelloWorldController.java | 31 +++++++--- src/main/resources/insert-data.sql | 2 + src/main/resources/schema.sql | 8 +++ 10 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 .gitignore create mode 100644 src/main/java/stenden/spring/configuration/DatabaseConfig.java create mode 100644 src/main/java/stenden/spring/hibernate/HibernateHouse.java create mode 100644 src/main/java/stenden/spring/hibernate/HibernateRepository.java create mode 100644 src/main/java/stenden/spring/jdbc/JdbcHouse.java create mode 100644 src/main/java/stenden/spring/jdbc/JdbcRepository.java create mode 100644 src/main/resources/insert-data.sql create mode 100644 src/main/resources/schema.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2acce55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Created by .ignore support plugin (hsz.mobi) +.idea/ diff --git a/pom.xml b/pom.xml index ce79af2..81e4ec2 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ war - 5.1.2.RELEASE + 5.1.3.RELEASE 3.8.0 1.7.0 3.2.2 @@ -24,6 +24,18 @@ ${spring.version} + + org.springframework + spring-jdbc + ${spring.version} + + + + org.springframework + spring-orm + ${spring.version} + + javax.servlet @@ -52,6 +64,20 @@ logback-classic 1.1.7 + + + + com.h2database + h2 + 1.4.197 + + + + org.hibernate + hibernate-core + 5.3.7.Final + + diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java new file mode 100644 index 0000000..faceb0d --- /dev/null +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -0,0 +1,62 @@ +package stenden.spring.configuration; + +import lombok.extern.slf4j.Slf4j; +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.orm.hibernate5.HibernateTransactionManager; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.sql.DataSource; +import java.util.Properties; + +@Slf4j +@Configuration +@EnableTransactionManagement // Required for Hibernate +public class DatabaseConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("classpath:schema.sql") + .addScript("classpath:insert-data.sql") + .build(); + } + + @Bean + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean + public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { + LocalSessionFactoryBean sfb = new LocalSessionFactoryBean(); + sfb.setDataSource(dataSource); + sfb.setPackagesToScan("stenden.spring.hibernate"); + Properties props = new Properties(); + props.setProperty("dialect", "org.hibernate.dialect.H2Dialect"); + sfb.setHibernateProperties(props); + return sfb; + } + + @Bean + public PlatformTransactionManager transactionManager(SessionFactory sessionFactory){ + HibernateTransactionManager transactionManager = new HibernateTransactionManager(); + transactionManager.setSessionFactory(sessionFactory); + return transactionManager; + } + + @Bean + public BeanPostProcessor persistenceTranslation() { + return new PersistenceExceptionTranslationPostProcessor(); + } +} diff --git a/src/main/java/stenden/spring/hibernate/HibernateHouse.java b/src/main/java/stenden/spring/hibernate/HibernateHouse.java new file mode 100644 index 0000000..0336781 --- /dev/null +++ b/src/main/java/stenden/spring/hibernate/HibernateHouse.java @@ -0,0 +1,35 @@ +package stenden.spring.hibernate; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "HOUSES") +@Entity +public class HibernateHouse { + + @Id + private Long id; + + @Column(name = "NR_OF_FLOORS") + private Integer nrOfFloors; + + @Column(name = "NR_OF_ROOMS") + private Integer nrOfRooms; + + @Column(name = "STREET") + private String street; + + @Column(name = "CITY") + private String city; + +} diff --git a/src/main/java/stenden/spring/hibernate/HibernateRepository.java b/src/main/java/stenden/spring/hibernate/HibernateRepository.java new file mode 100644 index 0000000..e62512c --- /dev/null +++ b/src/main/java/stenden/spring/hibernate/HibernateRepository.java @@ -0,0 +1,29 @@ +package stenden.spring.hibernate; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import javax.transaction.Transactional; + +@Repository +public class HibernateRepository { + + private SessionFactory sessionFactory; + + @Autowired + public HibernateRepository(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + private Session currentSession() { + return sessionFactory.getCurrentSession(); + } + + @Transactional + public HibernateHouse getByID(Long id) { + return currentSession().get(HibernateHouse.class, id); + } + +} diff --git a/src/main/java/stenden/spring/jdbc/JdbcHouse.java b/src/main/java/stenden/spring/jdbc/JdbcHouse.java new file mode 100644 index 0000000..3fde533 --- /dev/null +++ b/src/main/java/stenden/spring/jdbc/JdbcHouse.java @@ -0,0 +1,22 @@ +package stenden.spring.jdbc; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class JdbcHouse { + + private Long id; + + private Integer nrOfFloors; + + private Integer nrOfRooms; + + private String street; + + private String city; + +} diff --git a/src/main/java/stenden/spring/jdbc/JdbcRepository.java b/src/main/java/stenden/spring/jdbc/JdbcRepository.java new file mode 100644 index 0000000..1a88471 --- /dev/null +++ b/src/main/java/stenden/spring/jdbc/JdbcRepository.java @@ -0,0 +1,35 @@ +package stenden.spring.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.stereotype.Repository; + +import java.sql.ResultSet; +import java.sql.SQLException; + +@Repository +public class JdbcRepository { + + private static final String GET_HOUSE_BY_ID = + "SELECT ID, NR_OF_FLOORS, NR_OF_ROOMS, STREET, CITY FROM HOUSES WHERE ID = ?"; + + private JdbcOperations jdbcOperations; + + @Autowired + public JdbcRepository(JdbcOperations jdbcOperations) { + this.jdbcOperations = jdbcOperations; + } + + public JdbcHouse getByID(Long id) { + return jdbcOperations.queryForObject(GET_HOUSE_BY_ID, this::rowMapper, id); + } + + private JdbcHouse rowMapper(ResultSet rs, int rowNum) throws SQLException { + return new JdbcHouse(rs.getLong("ID"), + rs.getInt("NR_OF_FLOORS"), + rs.getInt("NR_OF_ROOMS"), + rs.getString("STREET"), + rs.getString("CITY")); + } + +} diff --git a/src/main/java/stenden/spring/resource/HelloWorldController.java b/src/main/java/stenden/spring/resource/HelloWorldController.java index 57510e7..08abbb1 100644 --- a/src/main/java/stenden/spring/resource/HelloWorldController.java +++ b/src/main/java/stenden/spring/resource/HelloWorldController.java @@ -1,10 +1,12 @@ package stenden.spring.resource; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import stenden.spring.hibernate.HibernateHouse; +import stenden.spring.hibernate.HibernateRepository; +import stenden.spring.jdbc.JdbcHouse; +import stenden.spring.jdbc.JdbcRepository; // We're telling Spring MVC that this is a REST controller // This treats methods with @RequestMapping as if it had the @ResponseBody annotation @@ -14,16 +16,25 @@ @RequestMapping("/hello_world") public class HelloWorldController { + private JdbcRepository jdbcRepository; + private HibernateRepository hibernateRepository; + // We have a greeting we read from the properties file using the Spring Expression Language @Value("${stenden.greeting}") private String greeting; + @Autowired + public HelloWorldController(JdbcRepository jdbcRepository, HibernateRepository hibernateRepository) { + this.jdbcRepository = jdbcRepository; + this.hibernateRepository = hibernateRepository; + } + /** * Finally, your first endpoint! And this is a GET endpoint, as you can see in the annotation * * @return a message with a greeting */ - @RequestMapping(method = RequestMethod.GET) + @GetMapping public Message helloWorld() { return new Message(greeting); } @@ -34,7 +45,7 @@ public Message helloWorld() { * @param name * @return */ - @RequestMapping(path = "/custom", method = RequestMethod.GET) + @GetMapping("/custom") public Message customGreeting(@RequestParam("name") String name) { return new Message("Hello " + name + "!"); } @@ -44,8 +55,14 @@ public Message customGreeting(@RequestParam("name") String name) { * * @return This method will always fail! */ - @RequestMapping(path = "/error", method = RequestMethod.GET) + @GetMapping("/error") public Message failedHelloWorld() { throw new GreetingException("I can't be bothered"); } + + @GetMapping("/houses/{id}") + public HibernateHouse getHouse(@PathVariable("id") Long id) { + return hibernateRepository.getByID(id); + } + } diff --git a/src/main/resources/insert-data.sql b/src/main/resources/insert-data.sql new file mode 100644 index 0000000..5187fcc --- /dev/null +++ b/src/main/resources/insert-data.sql @@ -0,0 +1,2 @@ +INSERT INTO HOUSES (NR_OF_FLOORS, NR_OF_ROOMS, STREET, CITY) +VALUES (4, 12, 'Ubbo Emmiussingel 112', 'Groningen'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..53ae414 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE HOUSES ( + ID int NOT NULL AUTO_INCREMENT, + NR_OF_FLOORS NUMBER, + NR_OF_ROOMS NUMBER, + STREET varchar(255) NOT NULL, + CITY varchar(255) NOT NULL, + PRIMARY KEY (ID) +); \ No newline at end of file From 122079138702d96ac898522afd01410d3505c732 Mon Sep 17 00:00:00 2001 From: qc80ja Date: Wed, 28 Nov 2018 16:11:02 +0100 Subject: [PATCH 02/18] REF: Created interfaces REF: Extracted the endpoints to a different resource --- .../spring/configuration/DatabaseConfig.java | 5 +- src/main/java/stenden/spring/data/House.java | 25 +++++++ .../stenden/spring/data/HouseRepository.java | 7 ++ .../{ => data}/hibernate/HibernateHouse.java | 5 +- .../hibernate/HibernateRepository.java | 8 ++- .../spring/{ => data}/jdbc/JdbcHouse.java | 5 +- .../{ => data}/jdbc/JdbcRepository.java | 8 ++- .../stenden/spring/resource/DataResource.java | 38 +++++++++++ .../spring/resource/HelloWorldController.java | 68 ------------------- .../spring/resource/HelloWorldResource.java | 50 ++++++++++++++ 10 files changed, 138 insertions(+), 81 deletions(-) create mode 100644 src/main/java/stenden/spring/data/House.java create mode 100644 src/main/java/stenden/spring/data/HouseRepository.java rename src/main/java/stenden/spring/{ => data}/hibernate/HibernateHouse.java (82%) rename src/main/java/stenden/spring/{ => data}/hibernate/HibernateRepository.java (72%) rename src/main/java/stenden/spring/{ => data}/jdbc/JdbcHouse.java (70%) rename src/main/java/stenden/spring/{ => data}/jdbc/JdbcRepository.java (81%) create mode 100644 src/main/java/stenden/spring/resource/DataResource.java delete mode 100644 src/main/java/stenden/spring/resource/HelloWorldController.java create mode 100644 src/main/java/stenden/spring/resource/HelloWorldResource.java diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index faceb0d..bce21d8 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -11,7 +11,6 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.hibernate5.HibernateTransactionManager; import org.springframework.orm.hibernate5.LocalSessionFactoryBean; -import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -41,7 +40,7 @@ public JdbcTemplate jdbcTemplate(DataSource dataSource) { public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean sfb = new LocalSessionFactoryBean(); sfb.setDataSource(dataSource); - sfb.setPackagesToScan("stenden.spring.hibernate"); + sfb.setPackagesToScan("stenden.spring.data.hibernate"); Properties props = new Properties(); props.setProperty("dialect", "org.hibernate.dialect.H2Dialect"); sfb.setHibernateProperties(props); @@ -49,7 +48,7 @@ public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { } @Bean - public PlatformTransactionManager transactionManager(SessionFactory sessionFactory){ + public PlatformTransactionManager transactionManager(SessionFactory sessionFactory) { HibernateTransactionManager transactionManager = new HibernateTransactionManager(); transactionManager.setSessionFactory(sessionFactory); return transactionManager; diff --git a/src/main/java/stenden/spring/data/House.java b/src/main/java/stenden/spring/data/House.java new file mode 100644 index 0000000..dadada6 --- /dev/null +++ b/src/main/java/stenden/spring/data/House.java @@ -0,0 +1,25 @@ +package stenden.spring.data; + +public interface House { + + public Long getId(); + + public Integer getNrOfFloors(); + + public Integer getNrOfRooms(); + + public String getStreet(); + + public String getCity(); + + public void setId(Long id); + + public void setNrOfFloors(Integer nrOfFloors); + + public void setNrOfRooms(Integer nrOfRooms); + + public void setStreet(String street); + + public void setCity(String city); + +} diff --git a/src/main/java/stenden/spring/data/HouseRepository.java b/src/main/java/stenden/spring/data/HouseRepository.java new file mode 100644 index 0000000..70c4d2d --- /dev/null +++ b/src/main/java/stenden/spring/data/HouseRepository.java @@ -0,0 +1,7 @@ +package stenden.spring.data; + +public interface HouseRepository { + + House getByID(Long id); + +} diff --git a/src/main/java/stenden/spring/hibernate/HibernateHouse.java b/src/main/java/stenden/spring/data/hibernate/HibernateHouse.java similarity index 82% rename from src/main/java/stenden/spring/hibernate/HibernateHouse.java rename to src/main/java/stenden/spring/data/hibernate/HibernateHouse.java index 0336781..6d21f23 100644 --- a/src/main/java/stenden/spring/hibernate/HibernateHouse.java +++ b/src/main/java/stenden/spring/data/hibernate/HibernateHouse.java @@ -1,9 +1,10 @@ -package stenden.spring.hibernate; +package stenden.spring.data.hibernate; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import stenden.spring.data.House; import javax.persistence.Column; import javax.persistence.Entity; @@ -15,7 +16,7 @@ @NoArgsConstructor @Table(name = "HOUSES") @Entity -public class HibernateHouse { +public class HibernateHouse implements House { @Id private Long id; diff --git a/src/main/java/stenden/spring/hibernate/HibernateRepository.java b/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java similarity index 72% rename from src/main/java/stenden/spring/hibernate/HibernateRepository.java rename to src/main/java/stenden/spring/data/hibernate/HibernateRepository.java index e62512c..f0622a0 100644 --- a/src/main/java/stenden/spring/hibernate/HibernateRepository.java +++ b/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java @@ -1,14 +1,16 @@ -package stenden.spring.hibernate; +package stenden.spring.data.hibernate; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; +import stenden.spring.data.House; +import stenden.spring.data.HouseRepository; import javax.transaction.Transactional; @Repository -public class HibernateRepository { +public class HibernateRepository implements HouseRepository { private SessionFactory sessionFactory; @@ -22,7 +24,7 @@ private Session currentSession() { } @Transactional - public HibernateHouse getByID(Long id) { + public House getByID(Long id) { return currentSession().get(HibernateHouse.class, id); } diff --git a/src/main/java/stenden/spring/jdbc/JdbcHouse.java b/src/main/java/stenden/spring/data/jdbc/JdbcHouse.java similarity index 70% rename from src/main/java/stenden/spring/jdbc/JdbcHouse.java rename to src/main/java/stenden/spring/data/jdbc/JdbcHouse.java index 3fde533..963dcff 100644 --- a/src/main/java/stenden/spring/jdbc/JdbcHouse.java +++ b/src/main/java/stenden/spring/data/jdbc/JdbcHouse.java @@ -1,13 +1,14 @@ -package stenden.spring.jdbc; +package stenden.spring.data.jdbc; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import stenden.spring.data.House; @Data @AllArgsConstructor @NoArgsConstructor -public class JdbcHouse { +public class JdbcHouse implements House { private Long id; diff --git a/src/main/java/stenden/spring/jdbc/JdbcRepository.java b/src/main/java/stenden/spring/data/jdbc/JdbcRepository.java similarity index 81% rename from src/main/java/stenden/spring/jdbc/JdbcRepository.java rename to src/main/java/stenden/spring/data/jdbc/JdbcRepository.java index 1a88471..f32da0f 100644 --- a/src/main/java/stenden/spring/jdbc/JdbcRepository.java +++ b/src/main/java/stenden/spring/data/jdbc/JdbcRepository.java @@ -1,14 +1,16 @@ -package stenden.spring.jdbc; +package stenden.spring.data.jdbc; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.stereotype.Repository; +import stenden.spring.data.House; +import stenden.spring.data.HouseRepository; import java.sql.ResultSet; import java.sql.SQLException; @Repository -public class JdbcRepository { +public class JdbcRepository implements HouseRepository { private static final String GET_HOUSE_BY_ID = "SELECT ID, NR_OF_FLOORS, NR_OF_ROOMS, STREET, CITY FROM HOUSES WHERE ID = ?"; @@ -20,7 +22,7 @@ public JdbcRepository(JdbcOperations jdbcOperations) { this.jdbcOperations = jdbcOperations; } - public JdbcHouse getByID(Long id) { + public House getByID(Long id) { return jdbcOperations.queryForObject(GET_HOUSE_BY_ID, this::rowMapper, id); } diff --git a/src/main/java/stenden/spring/resource/DataResource.java b/src/main/java/stenden/spring/resource/DataResource.java new file mode 100644 index 0000000..63d9732 --- /dev/null +++ b/src/main/java/stenden/spring/resource/DataResource.java @@ -0,0 +1,38 @@ +package stenden.spring.resource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import stenden.spring.data.House; +import stenden.spring.data.HouseRepository; + +// We're telling Spring MVC that this is a REST controller +// This treats methods with @RequestMapping as if it had the @ResponseBody annotation +// which tells Spring that the return value should be transformed into a response +@RestController +// The base request mapping this controller listens to, we respond to anything starting with /data +@RequestMapping("/data") +public class DataResource { + + private final HouseRepository jdbcRepository; + private final HouseRepository hibernateRepository; + + @Autowired + public DataResource(HouseRepository jdbcRepository, HouseRepository hibernateRepository) { + this.jdbcRepository = jdbcRepository; + this.hibernateRepository = hibernateRepository; + } + + @GetMapping("/jdbc/{id}") + public House getJdbcHouse(@PathVariable("id") Long id) { + return jdbcRepository.getByID(id); + } + + @GetMapping("/hibernate/{id}") + public House getHibernateHouse(@PathVariable("id") Long id) { + return hibernateRepository.getByID(id); + } + +} diff --git a/src/main/java/stenden/spring/resource/HelloWorldController.java b/src/main/java/stenden/spring/resource/HelloWorldController.java deleted file mode 100644 index 08abbb1..0000000 --- a/src/main/java/stenden/spring/resource/HelloWorldController.java +++ /dev/null @@ -1,68 +0,0 @@ -package stenden.spring.resource; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.*; -import stenden.spring.hibernate.HibernateHouse; -import stenden.spring.hibernate.HibernateRepository; -import stenden.spring.jdbc.JdbcHouse; -import stenden.spring.jdbc.JdbcRepository; - -// We're telling Spring MVC that this is a REST controller -// This treats methods with @RequestMapping as if it had the @ResponseBody annotation -// which tells Spring that the return value should be transformed into a response -@RestController -// The base request mapping this controller listens to, we respond to anything starting with /hello_world -@RequestMapping("/hello_world") -public class HelloWorldController { - - private JdbcRepository jdbcRepository; - private HibernateRepository hibernateRepository; - - // We have a greeting we read from the properties file using the Spring Expression Language - @Value("${stenden.greeting}") - private String greeting; - - @Autowired - public HelloWorldController(JdbcRepository jdbcRepository, HibernateRepository hibernateRepository) { - this.jdbcRepository = jdbcRepository; - this.hibernateRepository = hibernateRepository; - } - - /** - * Finally, your first endpoint! And this is a GET endpoint, as you can see in the annotation - * - * @return a message with a greeting - */ - @GetMapping - public Message helloWorld() { - return new Message(greeting); - } - - /** - * Now let's grab a parameter from the URL - * - * @param name - * @return - */ - @GetMapping("/custom") - public Message customGreeting(@RequestParam("name") String name) { - return new Message("Hello " + name + "!"); - } - - /** - * Let's demonstrate an exception - * - * @return This method will always fail! - */ - @GetMapping("/error") - public Message failedHelloWorld() { - throw new GreetingException("I can't be bothered"); - } - - @GetMapping("/houses/{id}") - public HibernateHouse getHouse(@PathVariable("id") Long id) { - return hibernateRepository.getByID(id); - } - -} diff --git a/src/main/java/stenden/spring/resource/HelloWorldResource.java b/src/main/java/stenden/spring/resource/HelloWorldResource.java new file mode 100644 index 0000000..d92d577 --- /dev/null +++ b/src/main/java/stenden/spring/resource/HelloWorldResource.java @@ -0,0 +1,50 @@ +package stenden.spring.resource; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; +import stenden.spring.data.House; + +// We're telling Spring MVC that this is a REST controller +// This treats methods with @RequestMapping as if it had the @ResponseBody annotation +// which tells Spring that the return value should be transformed into a response +@RestController +// The base request mapping this controller listens to, we respond to anything starting with /hello_world +@RequestMapping("/hello_world") +public class HelloWorldResource { + + // We have a greeting we read from the properties file using the Spring Expression Language + @Value("${stenden.greeting}") + private String greeting; + + /** + * Finally, your first endpoint! And this is a GET endpoint, as you can see in the annotation + * + * @return a message with a greeting + */ + @GetMapping + public Message helloWorld() { + return new Message(greeting); + } + + /** + * Now let's grab a parameter from the URL + * + * @param name + * @return + */ + @GetMapping("/custom") + public Message customGreeting(@RequestParam("name") String name) { + return new Message("Hello " + name + "!"); + } + + /** + * Let's demonstrate an exception + * + * @return This method will always fail! + */ + @GetMapping("/error") + public Message failedHelloWorld() { + throw new GreetingException("I can't be bothered"); + } + +} From e8da4e3a1e874997aa6310f88b4396d2a25316eb Mon Sep 17 00:00:00 2001 From: qc80ja Date: Thu, 29 Nov 2018 16:06:59 +0100 Subject: [PATCH 03/18] ADD: Basic JDBC Example --- .../java/stenden/spring/HouseService.java | 22 +++++++ .../spring/configuration/DatabaseConfig.java | 15 +++++ ...itory.java => JdbcTemplateRepository.java} | 4 +- .../spring/data/jdbc/PureJdbcRepository.java | 57 +++++++++++++++++++ 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/main/java/stenden/spring/HouseService.java rename src/main/java/stenden/spring/data/jdbc/{JdbcRepository.java => JdbcTemplateRepository.java} (88%) create mode 100644 src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java diff --git a/src/main/java/stenden/spring/HouseService.java b/src/main/java/stenden/spring/HouseService.java new file mode 100644 index 0000000..fdc5612 --- /dev/null +++ b/src/main/java/stenden/spring/HouseService.java @@ -0,0 +1,22 @@ +package stenden.spring; + +import org.springframework.stereotype.Service; +import stenden.spring.data.House; + +@Service +public class HouseService { + + public House getJdbcHouse(Long id) { + return null; + } + + public House getHibernateHouse(Long id) { + return null; + } + + public House getPureJdbcHouse(Long id) { + return null; + } + +} + diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index bce21d8..99af3ed 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -22,6 +22,12 @@ @EnableTransactionManagement // Required for Hibernate public class DatabaseConfig { + /** + * The {@link DataSource} representing the database connection. + * In our case we're creating an in-memory database using H2, + * so the setup is simple. + * @return The connection to the database + */ @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() @@ -31,6 +37,15 @@ public DataSource dataSource() { .build(); } + /** + * JDBC by itself is tough to use, so we wrap it in the {@link JdbcTemplate} from Spring, + * which handles a lot of the boilerplate for us. + * Pure JDBC has its uses though. While it gives a lot of boilerplate, + * it also gives a lot of control, which can be useful for certain applications. + * Think of having to make very complex queries for very specific situations. + * @param dataSource + * @return + */ @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); diff --git a/src/main/java/stenden/spring/data/jdbc/JdbcRepository.java b/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java similarity index 88% rename from src/main/java/stenden/spring/data/jdbc/JdbcRepository.java rename to src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java index f32da0f..6dbd37c 100644 --- a/src/main/java/stenden/spring/data/jdbc/JdbcRepository.java +++ b/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java @@ -10,7 +10,7 @@ import java.sql.SQLException; @Repository -public class JdbcRepository implements HouseRepository { +public class JdbcTemplateRepository implements HouseRepository { private static final String GET_HOUSE_BY_ID = "SELECT ID, NR_OF_FLOORS, NR_OF_ROOMS, STREET, CITY FROM HOUSES WHERE ID = ?"; @@ -18,7 +18,7 @@ public class JdbcRepository implements HouseRepository { private JdbcOperations jdbcOperations; @Autowired - public JdbcRepository(JdbcOperations jdbcOperations) { + public JdbcTemplateRepository(JdbcOperations jdbcOperations) { this.jdbcOperations = jdbcOperations; } diff --git a/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java b/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java new file mode 100644 index 0000000..eb5fca1 --- /dev/null +++ b/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java @@ -0,0 +1,57 @@ +package stenden.spring.data.jdbc; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import stenden.spring.data.House; +import stenden.spring.data.HouseRepository; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +@Slf4j +@Repository +public class PureJdbcRepository implements HouseRepository { + + private static final String GET_HOUSE_BY_ID = + "SELECT ID, NR_OF_FLOORS, NR_OF_ROOMS, STREET, CITY FROM HOUSES WHERE ID = ?"; + + private DataSource dataSource; + + @Autowired + public PureJdbcRepository(DataSource dataSource) { + this.dataSource = dataSource; + } + + public House getByID(Long id) { + try ( + Connection connection = dataSource.getConnection(); + PreparedStatement statement = createPreparedStatement(connection, id); + ResultSet resultSet = statement.executeQuery() + ) { + resultSet.first(); + return new JdbcHouse( + resultSet.getLong("ID"), + resultSet.getInt("NR_OF_FLOORS"), + resultSet.getInt("NR_OF_ROOMS"), + resultSet.getString("STREET"), + resultSet.getString("CITY") + ); + } catch (SQLException e) { + // Any kind of exception thrown is an SQLException. So it could be anything... + log.error("The query failed!", e); + } + + return null; + } + + private PreparedStatement createPreparedStatement(Connection connection, Long id) throws SQLException { + try (PreparedStatement statement = connection.prepareStatement(GET_HOUSE_BY_ID)) { + statement.setLong(1, id); + return statement; + } + } +} From 23ebad01ff837b5f3eb84b0602465c98e9dc9791 Mon Sep 17 00:00:00 2001 From: qc80ja Date: Fri, 30 Nov 2018 13:54:44 +0100 Subject: [PATCH 04/18] ADD: JPA Examples ENH: Formatting --- pom.xml | 7 +++ .../configuration/ApplicationInitializer.java | 60 +++++++++---------- .../spring/configuration/DatabaseConfig.java | 26 +++++++- src/main/java/stenden/spring/data/House.java | 20 +++---- .../data/hibernate/HibernateRepository.java | 3 +- .../data/jdbc/JdbcTemplateRepository.java | 5 +- .../spring/data/jdbc/PureJdbcRepository.java | 14 ++--- .../data/jpa/EntityManagerJpaRepository.java | 32 ++++++++++ .../spring/data/jpa/SpringJpaRepository.java | 7 +++ .../AnnotatedHouse.java} | 4 +- .../JdbcHouse.java => model/POJOHouse.java} | 4 +- .../stenden/spring/resource/DataResource.java | 38 ++++++++++-- .../spring/resource/ErrorResponse.java | 2 +- .../spring/resource/ExceptionHandlers.java | 10 ++-- .../spring/resource/GreetingException.java | 6 +- .../spring/resource/HelloWorldResource.java | 6 +- .../java/stenden/spring/resource/Message.java | 2 +- 17 files changed, 173 insertions(+), 73 deletions(-) create mode 100644 src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java create mode 100644 src/main/java/stenden/spring/data/jpa/SpringJpaRepository.java rename src/main/java/stenden/spring/data/{hibernate/HibernateHouse.java => model/AnnotatedHouse.java} (87%) rename src/main/java/stenden/spring/data/{jdbc/JdbcHouse.java => model/POJOHouse.java} (79%) diff --git a/pom.xml b/pom.xml index 81e4ec2..9f0cd4e 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ 1.7.0 3.2.2 9.0.13 + 2.1.3.RELEASE @@ -36,6 +37,12 @@ ${spring.version} + + org.springframework.data + spring-data-jpa + ${spring-data-jpa.version} + + javax.servlet diff --git a/src/main/java/stenden/spring/configuration/ApplicationInitializer.java b/src/main/java/stenden/spring/configuration/ApplicationInitializer.java index daeaff3..67f5b53 100644 --- a/src/main/java/stenden/spring/configuration/ApplicationInitializer.java +++ b/src/main/java/stenden/spring/configuration/ApplicationInitializer.java @@ -7,36 +7,36 @@ */ public class ApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - /** - * We accept all incoming requests starting at / - * - * @return All the mappings we accept - */ - @Override - protected String[] getServletMappings() { - return new String[]{"/"}; - } + /** + * We accept all incoming requests starting at / + * + * @return All the mappings we accept + */ + @Override + protected String[] getServletMappings() { + return new String[]{"/"}; + } - /** - * The classes we use for configuring our ApplicationContext - * - * @return An array containing configuration classes for our ApplicationContext - */ - @Override - protected Class[] getRootConfigClasses() { - return new Class[]{WebConfig.class}; - } + /** + * The classes we use for configuring our ApplicationContext + * + * @return An array containing configuration classes for our ApplicationContext + */ + @Override + protected Class[] getRootConfigClasses() { + return new Class[]{WebConfig.class}; + } - /** - * We have no special DispatcherServlet logic, so we do all the configuration in {@link WebConfig} - * It is useful if you have multiple DispatcherServlets and you want to specifically manage the different - * WebApplicationContexts - * https://stackoverflow.com/questions/35258758/getservletconfigclasses-vs-getrootconfigclasses-when-extending-abstractannot - * - * @return null, as it's not necessary for us. - */ - @Override - protected Class[] getServletConfigClasses() { - return null; - } + /** + * We have no special DispatcherServlet logic, so we do all the configuration in {@link WebConfig} + * It is useful if you have multiple DispatcherServlets and you want to specifically manage the different + * WebApplicationContexts + * https://stackoverflow.com/questions/35258758/getservletconfigclasses-vs-getrootconfigclasses-when-extending-abstractannot + * + * @return null, as it's not necessary for us. + */ + @Override + protected Class[] getServletConfigClasses() { + return null; + } } diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index 99af3ed..283fabc 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -6,11 +6,16 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.hibernate5.HibernateTransactionManager; import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -20,12 +25,14 @@ @Slf4j @Configuration @EnableTransactionManagement // Required for Hibernate +@EnableJpaRepositories("stenden.spring.data") public class DatabaseConfig { /** * The {@link DataSource} representing the database connection. * In our case we're creating an in-memory database using H2, * so the setup is simple. + * * @return The connection to the database */ @Bean @@ -43,6 +50,7 @@ public DataSource dataSource() { * Pure JDBC has its uses though. While it gives a lot of boilerplate, * it also gives a lot of control, which can be useful for certain applications. * Think of having to make very complex queries for very specific situations. + * * @param dataSource * @return */ @@ -55,7 +63,7 @@ public JdbcTemplate jdbcTemplate(DataSource dataSource) { public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { LocalSessionFactoryBean sfb = new LocalSessionFactoryBean(); sfb.setDataSource(dataSource); - sfb.setPackagesToScan("stenden.spring.data.hibernate"); + sfb.setPackagesToScan("stenden.spring.data.model"); Properties props = new Properties(); props.setProperty("dialect", "org.hibernate.dialect.H2Dialect"); sfb.setHibernateProperties(props); @@ -73,4 +81,20 @@ public PlatformTransactionManager transactionManager(SessionFactory sessionFacto public BeanPostProcessor persistenceTranslation() { return new PersistenceExceptionTranslationPostProcessor(); } + + @Bean + public JpaVendorAdapter jpaVendorAdapter() { + HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter(); + adapter.setDatabase(Database.H2); + return adapter; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter); + entityManagerFactoryBean.setPackagesToScan("stenden.spring.data.model"); + return entityManagerFactoryBean; + } } diff --git a/src/main/java/stenden/spring/data/House.java b/src/main/java/stenden/spring/data/House.java index dadada6..b6f538a 100644 --- a/src/main/java/stenden/spring/data/House.java +++ b/src/main/java/stenden/spring/data/House.java @@ -2,24 +2,24 @@ public interface House { - public Long getId(); + Long getId(); - public Integer getNrOfFloors(); + void setId(Long id); - public Integer getNrOfRooms(); + Integer getNrOfFloors(); - public String getStreet(); + void setNrOfFloors(Integer nrOfFloors); - public String getCity(); + Integer getNrOfRooms(); - public void setId(Long id); + void setNrOfRooms(Integer nrOfRooms); - public void setNrOfFloors(Integer nrOfFloors); + String getStreet(); - public void setNrOfRooms(Integer nrOfRooms); + void setStreet(String street); - public void setStreet(String street); + String getCity(); - public void setCity(String city); + void setCity(String city); } diff --git a/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java b/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java index f0622a0..af40618 100644 --- a/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java +++ b/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Repository; import stenden.spring.data.House; import stenden.spring.data.HouseRepository; +import stenden.spring.data.model.AnnotatedHouse; import javax.transaction.Transactional; @@ -25,7 +26,7 @@ private Session currentSession() { @Transactional public House getByID(Long id) { - return currentSession().get(HibernateHouse.class, id); + return currentSession().get(AnnotatedHouse.class, id); } } diff --git a/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java b/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java index 6dbd37c..19c2e52 100644 --- a/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java +++ b/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository; import stenden.spring.data.House; import stenden.spring.data.HouseRepository; +import stenden.spring.data.model.POJOHouse; import java.sql.ResultSet; import java.sql.SQLException; @@ -26,8 +27,8 @@ public House getByID(Long id) { return jdbcOperations.queryForObject(GET_HOUSE_BY_ID, this::rowMapper, id); } - private JdbcHouse rowMapper(ResultSet rs, int rowNum) throws SQLException { - return new JdbcHouse(rs.getLong("ID"), + private POJOHouse rowMapper(ResultSet rs, int rowNum) throws SQLException { + return new POJOHouse(rs.getLong("ID"), rs.getInt("NR_OF_FLOORS"), rs.getInt("NR_OF_ROOMS"), rs.getString("STREET"), diff --git a/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java b/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java index eb5fca1..14da3e2 100644 --- a/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java +++ b/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java @@ -2,9 +2,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.stereotype.Repository; import stenden.spring.data.House; import stenden.spring.data.HouseRepository; +import stenden.spring.data.model.POJOHouse; import javax.sql.DataSource; import java.sql.Connection; @@ -33,7 +35,7 @@ public House getByID(Long id) { ResultSet resultSet = statement.executeQuery() ) { resultSet.first(); - return new JdbcHouse( + return new POJOHouse( resultSet.getLong("ID"), resultSet.getInt("NR_OF_FLOORS"), resultSet.getInt("NR_OF_ROOMS"), @@ -43,15 +45,13 @@ public House getByID(Long id) { } catch (SQLException e) { // Any kind of exception thrown is an SQLException. So it could be anything... log.error("The query failed!", e); + throw new UncategorizedSQLException("Querying for a house", GET_HOUSE_BY_ID, e); } - - return null; } private PreparedStatement createPreparedStatement(Connection connection, Long id) throws SQLException { - try (PreparedStatement statement = connection.prepareStatement(GET_HOUSE_BY_ID)) { - statement.setLong(1, id); - return statement; - } + PreparedStatement statement = connection.prepareStatement(GET_HOUSE_BY_ID); + statement.setLong(1, id); + return statement; } } diff --git a/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java b/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java new file mode 100644 index 0000000..05c4d3e --- /dev/null +++ b/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java @@ -0,0 +1,32 @@ +package stenden.spring.data.jpa; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Repository; +import stenden.spring.data.House; +import stenden.spring.data.HouseRepository; +import stenden.spring.data.model.AnnotatedHouse; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; + +@Repository +@AllArgsConstructor +@NoArgsConstructor +public class EntityManagerJpaRepository implements HouseRepository { + + /** + * This gives us an EntityManager. + * Or, well, a proxy to one. Which gives or creates a thread-safe EntityManager for us + * every time we use it. + */ + @PersistenceContext(unitName = "entityManagerFactory") + private EntityManager em; + + @Override + @Transactional + public House getByID(Long id) { + return em.find(AnnotatedHouse.class, id); + } +} diff --git a/src/main/java/stenden/spring/data/jpa/SpringJpaRepository.java b/src/main/java/stenden/spring/data/jpa/SpringJpaRepository.java new file mode 100644 index 0000000..3b69e8e --- /dev/null +++ b/src/main/java/stenden/spring/data/jpa/SpringJpaRepository.java @@ -0,0 +1,7 @@ +package stenden.spring.data.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import stenden.spring.data.model.AnnotatedHouse; + +public interface SpringJpaRepository extends JpaRepository { +} diff --git a/src/main/java/stenden/spring/data/hibernate/HibernateHouse.java b/src/main/java/stenden/spring/data/model/AnnotatedHouse.java similarity index 87% rename from src/main/java/stenden/spring/data/hibernate/HibernateHouse.java rename to src/main/java/stenden/spring/data/model/AnnotatedHouse.java index 6d21f23..6072540 100644 --- a/src/main/java/stenden/spring/data/hibernate/HibernateHouse.java +++ b/src/main/java/stenden/spring/data/model/AnnotatedHouse.java @@ -1,4 +1,4 @@ -package stenden.spring.data.hibernate; +package stenden.spring.data.model; import lombok.AllArgsConstructor; @@ -16,7 +16,7 @@ @NoArgsConstructor @Table(name = "HOUSES") @Entity -public class HibernateHouse implements House { +public class AnnotatedHouse implements House { @Id private Long id; diff --git a/src/main/java/stenden/spring/data/jdbc/JdbcHouse.java b/src/main/java/stenden/spring/data/model/POJOHouse.java similarity index 79% rename from src/main/java/stenden/spring/data/jdbc/JdbcHouse.java rename to src/main/java/stenden/spring/data/model/POJOHouse.java index 963dcff..0e5acff 100644 --- a/src/main/java/stenden/spring/data/jdbc/JdbcHouse.java +++ b/src/main/java/stenden/spring/data/model/POJOHouse.java @@ -1,4 +1,4 @@ -package stenden.spring.data.jdbc; +package stenden.spring.data.model; import lombok.AllArgsConstructor; import lombok.Data; @@ -8,7 +8,7 @@ @Data @AllArgsConstructor @NoArgsConstructor -public class JdbcHouse implements House { +public class POJOHouse implements House { private Long id; diff --git a/src/main/java/stenden/spring/resource/DataResource.java b/src/main/java/stenden/spring/resource/DataResource.java index 63d9732..b65e6b1 100644 --- a/src/main/java/stenden/spring/resource/DataResource.java +++ b/src/main/java/stenden/spring/resource/DataResource.java @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RestController; import stenden.spring.data.House; import stenden.spring.data.HouseRepository; +import stenden.spring.data.jpa.SpringJpaRepository; // We're telling Spring MVC that this is a REST controller // This treats methods with @RequestMapping as if it had the @ResponseBody annotation @@ -16,18 +17,33 @@ @RequestMapping("/data") public class DataResource { - private final HouseRepository jdbcRepository; + private final HouseRepository jdbcTemplateRepository; + private final HouseRepository pureJdbcRepository; private final HouseRepository hibernateRepository; + private final HouseRepository entityManagerJpaRepository; + private final SpringJpaRepository springJpaRepository; @Autowired - public DataResource(HouseRepository jdbcRepository, HouseRepository hibernateRepository) { - this.jdbcRepository = jdbcRepository; + public DataResource(HouseRepository jdbcTemplateRepository, + HouseRepository pureJdbcRepository, + HouseRepository hibernateRepository, + HouseRepository entityManagerJpaRepository, + SpringJpaRepository springJpaRepository) { + this.jdbcTemplateRepository = jdbcTemplateRepository; + this.pureJdbcRepository = pureJdbcRepository; this.hibernateRepository = hibernateRepository; + this.entityManagerJpaRepository = entityManagerJpaRepository; + this.springJpaRepository = springJpaRepository; } - @GetMapping("/jdbc/{id}") - public House getJdbcHouse(@PathVariable("id") Long id) { - return jdbcRepository.getByID(id); + @GetMapping("/templatejdbc/{id}") + public House getTemplateJdbcHouse(@PathVariable("id") Long id) { + return jdbcTemplateRepository.getByID(id); + } + + @GetMapping("/purejdbc/{id}") + public House getPureJdbcHouse(@PathVariable("id") Long id) { + return pureJdbcRepository.getByID(id); } @GetMapping("/hibernate/{id}") @@ -35,4 +51,14 @@ public House getHibernateHouse(@PathVariable("id") Long id) { return hibernateRepository.getByID(id); } + @GetMapping("/jpa/{id}") + public House getJpaHouse(@PathVariable("id") Long id) { + return entityManagerJpaRepository.getByID(id); + } + + @GetMapping("/springjpa/{id}") + public House getSpringJpaHouse(@PathVariable("id") Long id) { + return springJpaRepository.findById(id).get(); + } + } diff --git a/src/main/java/stenden/spring/resource/ErrorResponse.java b/src/main/java/stenden/spring/resource/ErrorResponse.java index 85b8d67..cf12d5c 100644 --- a/src/main/java/stenden/spring/resource/ErrorResponse.java +++ b/src/main/java/stenden/spring/resource/ErrorResponse.java @@ -7,6 +7,6 @@ @AllArgsConstructor public class ErrorResponse { - private String error; + private String error; } diff --git a/src/main/java/stenden/spring/resource/ExceptionHandlers.java b/src/main/java/stenden/spring/resource/ExceptionHandlers.java index 75573e3..2c9cfe2 100644 --- a/src/main/java/stenden/spring/resource/ExceptionHandlers.java +++ b/src/main/java/stenden/spring/resource/ExceptionHandlers.java @@ -13,10 +13,10 @@ @RestControllerAdvice public class ExceptionHandlers { - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(GreetingException.class) - public ErrorResponse handleGreetingException(GreetingException exception, HttpServletRequest request) { - return new ErrorResponse(String.format("I have the message '%s' for %s", exception.getMessage(), request.getRemoteAddr())); - } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(GreetingException.class) + public ErrorResponse handleGreetingException(GreetingException exception, HttpServletRequest request) { + return new ErrorResponse(String.format("I have the message '%s' for %s", exception.getMessage(), request.getRemoteAddr())); + } } diff --git a/src/main/java/stenden/spring/resource/GreetingException.java b/src/main/java/stenden/spring/resource/GreetingException.java index 306eda0..f43f9de 100644 --- a/src/main/java/stenden/spring/resource/GreetingException.java +++ b/src/main/java/stenden/spring/resource/GreetingException.java @@ -2,8 +2,8 @@ public class GreetingException extends RuntimeException { - public GreetingException(String message) { - super(message); - } + public GreetingException(String message) { + super(message); + } } diff --git a/src/main/java/stenden/spring/resource/HelloWorldResource.java b/src/main/java/stenden/spring/resource/HelloWorldResource.java index d92d577..bbd3aca 100644 --- a/src/main/java/stenden/spring/resource/HelloWorldResource.java +++ b/src/main/java/stenden/spring/resource/HelloWorldResource.java @@ -1,8 +1,10 @@ package stenden.spring.resource; import org.springframework.beans.factory.annotation.Value; -import org.springframework.web.bind.annotation.*; -import stenden.spring.data.House; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; // We're telling Spring MVC that this is a REST controller // This treats methods with @RequestMapping as if it had the @ResponseBody annotation diff --git a/src/main/java/stenden/spring/resource/Message.java b/src/main/java/stenden/spring/resource/Message.java index fad1a93..9cad62c 100644 --- a/src/main/java/stenden/spring/resource/Message.java +++ b/src/main/java/stenden/spring/resource/Message.java @@ -7,6 +7,6 @@ @AllArgsConstructor public class Message { - private String message; + private String message; } From e3f411c92f8892c591dc4e7ff24968a486019691 Mon Sep 17 00:00:00 2001 From: qc80ja Date: Fri, 30 Nov 2018 15:42:53 +0100 Subject: [PATCH 05/18] ADD: Example of criteria building ADD: Example of different transaction manager --- .../spring/configuration/DatabaseConfig.java | 12 ++++++++++++ .../data/jpa/EntityManagerJpaRepository.java | 14 +++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index 283fabc..dc4b3f5 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -12,6 +12,7 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.hibernate5.HibernateTransactionManager; import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.Database; @@ -77,6 +78,17 @@ public PlatformTransactionManager transactionManager(SessionFactory sessionFacto return transactionManager; } + /** + * Whhen just using JPA, you could also use this transaction manager. + * @return + */ +// @Bean +// public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { +// JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); +// jpaTransactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject()); +// return jpaTransactionManager; +// } + @Bean public BeanPostProcessor persistenceTranslation() { return new PersistenceExceptionTranslationPostProcessor(); diff --git a/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java b/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java index 05c4d3e..5775898 100644 --- a/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java +++ b/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java @@ -9,6 +9,10 @@ import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.ParameterExpression; +import javax.persistence.criteria.Root; import javax.transaction.Transactional; @Repository @@ -27,6 +31,14 @@ public class EntityManagerJpaRepository implements HouseRepository { @Override @Transactional public House getByID(Long id) { - return em.find(AnnotatedHouse.class, id); + CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); + CriteriaQuery query = criteriaBuilder.createQuery(AnnotatedHouse.class); + Root from = query.from(AnnotatedHouse.class); + ParameterExpression parameter = criteriaBuilder.parameter(Long.class); + query.select(from).where(criteriaBuilder.equal(from.get("id"), parameter)); + + return em.createQuery(query).setParameter(parameter, id).getSingleResult(); + // A simpler way of doing it +// return em.find(AnnotatedHouse.class, id); } } From 9face36d1f0c4ef69089c34b93bd2069bb42667d Mon Sep 17 00:00:00 2001 From: MasterFenrir Date: Sat, 1 Dec 2018 22:02:14 +0100 Subject: [PATCH 06/18] REF: Added a service layer --- .../java/stenden/spring/HouseService.java | 22 -------- .../{DataResource.java => HouseResource.java} | 33 ++++------- .../stenden/spring/service/HouseService.java | 55 +++++++++++++++++++ 3 files changed, 65 insertions(+), 45 deletions(-) delete mode 100644 src/main/java/stenden/spring/HouseService.java rename src/main/java/stenden/spring/resource/{DataResource.java => HouseResource.java} (50%) create mode 100644 src/main/java/stenden/spring/service/HouseService.java diff --git a/src/main/java/stenden/spring/HouseService.java b/src/main/java/stenden/spring/HouseService.java deleted file mode 100644 index fdc5612..0000000 --- a/src/main/java/stenden/spring/HouseService.java +++ /dev/null @@ -1,22 +0,0 @@ -package stenden.spring; - -import org.springframework.stereotype.Service; -import stenden.spring.data.House; - -@Service -public class HouseService { - - public House getJdbcHouse(Long id) { - return null; - } - - public House getHibernateHouse(Long id) { - return null; - } - - public House getPureJdbcHouse(Long id) { - return null; - } - -} - diff --git a/src/main/java/stenden/spring/resource/DataResource.java b/src/main/java/stenden/spring/resource/HouseResource.java similarity index 50% rename from src/main/java/stenden/spring/resource/DataResource.java rename to src/main/java/stenden/spring/resource/HouseResource.java index b65e6b1..0cd5aae 100644 --- a/src/main/java/stenden/spring/resource/DataResource.java +++ b/src/main/java/stenden/spring/resource/HouseResource.java @@ -6,8 +6,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import stenden.spring.data.House; -import stenden.spring.data.HouseRepository; -import stenden.spring.data.jpa.SpringJpaRepository; +import stenden.spring.service.HouseService; // We're telling Spring MVC that this is a REST controller // This treats methods with @RequestMapping as if it had the @ResponseBody annotation @@ -15,50 +14,38 @@ @RestController // The base request mapping this controller listens to, we respond to anything starting with /data @RequestMapping("/data") -public class DataResource { +public class HouseResource { - private final HouseRepository jdbcTemplateRepository; - private final HouseRepository pureJdbcRepository; - private final HouseRepository hibernateRepository; - private final HouseRepository entityManagerJpaRepository; - private final SpringJpaRepository springJpaRepository; + private final HouseService houseService; @Autowired - public DataResource(HouseRepository jdbcTemplateRepository, - HouseRepository pureJdbcRepository, - HouseRepository hibernateRepository, - HouseRepository entityManagerJpaRepository, - SpringJpaRepository springJpaRepository) { - this.jdbcTemplateRepository = jdbcTemplateRepository; - this.pureJdbcRepository = pureJdbcRepository; - this.hibernateRepository = hibernateRepository; - this.entityManagerJpaRepository = entityManagerJpaRepository; - this.springJpaRepository = springJpaRepository; + public HouseResource(HouseService houseService) { + this.houseService = houseService; } @GetMapping("/templatejdbc/{id}") public House getTemplateJdbcHouse(@PathVariable("id") Long id) { - return jdbcTemplateRepository.getByID(id); + return houseService.getTemplateJdbcHouse(id); } @GetMapping("/purejdbc/{id}") public House getPureJdbcHouse(@PathVariable("id") Long id) { - return pureJdbcRepository.getByID(id); + return houseService.getPureJdbcHouse(id); } @GetMapping("/hibernate/{id}") public House getHibernateHouse(@PathVariable("id") Long id) { - return hibernateRepository.getByID(id); + return houseService.getHibernateHouse(id); } @GetMapping("/jpa/{id}") public House getJpaHouse(@PathVariable("id") Long id) { - return entityManagerJpaRepository.getByID(id); + return houseService.getJpaHouse(id); } @GetMapping("/springjpa/{id}") public House getSpringJpaHouse(@PathVariable("id") Long id) { - return springJpaRepository.findById(id).get(); + return houseService.getSpringJpaHouse(id); } } diff --git a/src/main/java/stenden/spring/service/HouseService.java b/src/main/java/stenden/spring/service/HouseService.java new file mode 100644 index 0000000..37d2f91 --- /dev/null +++ b/src/main/java/stenden/spring/service/HouseService.java @@ -0,0 +1,55 @@ +package stenden.spring.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import stenden.spring.data.House; +import stenden.spring.data.HouseRepository; +import stenden.spring.data.jpa.SpringJpaRepository; + +import javax.persistence.EntityNotFoundException; + +@Service +public class HouseService { + + private final HouseRepository jdbcTemplateRepository; + private final HouseRepository pureJdbcRepository; + private final HouseRepository hibernateRepository; + private final HouseRepository entityManagerJpaRepository; + private final SpringJpaRepository springJpaRepository; + + @Autowired + public HouseService(HouseRepository jdbcTemplateRepository, + HouseRepository pureJdbcRepository, + HouseRepository hibernateRepository, + HouseRepository entityManagerJpaRepository, + SpringJpaRepository springJpaRepository) { + this.jdbcTemplateRepository = jdbcTemplateRepository; + this.pureJdbcRepository = pureJdbcRepository; + this.hibernateRepository = hibernateRepository; + this.entityManagerJpaRepository = entityManagerJpaRepository; + this.springJpaRepository = springJpaRepository; + } + + public House getTemplateJdbcHouse(Long id) { + return jdbcTemplateRepository.getByID(id); + } + + public House getPureJdbcHouse(Long id) { + return pureJdbcRepository.getByID(id); + } + + public House getHibernateHouse(Long id) { + return hibernateRepository.getByID(id); + } + + public House getJpaHouse(Long id) { + return entityManagerJpaRepository.getByID(id); + } + + public House getSpringJpaHouse(Long id) { + return springJpaRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("House with ID " + id + " not found")); + } + +} + From af9c151c86134f7dbe75715fcd439dd7d5211ffa Mon Sep 17 00:00:00 2001 From: MasterFenrir Date: Sat, 1 Dec 2018 22:03:32 +0100 Subject: [PATCH 07/18] ENH: Optimize imports --- .../java/stenden/spring/configuration/DatabaseConfig.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index dc4b3f5..fa115ee 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -12,7 +12,6 @@ import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.hibernate5.HibernateTransactionManager; import org.springframework.orm.hibernate5.LocalSessionFactoryBean; -import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.Database; @@ -79,7 +78,8 @@ public PlatformTransactionManager transactionManager(SessionFactory sessionFacto } /** - * Whhen just using JPA, you could also use this transaction manager. + * Whhn just using JPA, you could also use this transaction manager. + * * @return */ // @Bean @@ -88,7 +88,6 @@ public PlatformTransactionManager transactionManager(SessionFactory sessionFacto // jpaTransactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject()); // return jpaTransactionManager; // } - @Bean public BeanPostProcessor persistenceTranslation() { return new PersistenceExceptionTranslationPostProcessor(); From 8a6723d44bb6743e5fe9e9e14b7ce59a9b66204d Mon Sep 17 00:00:00 2001 From: Sander ten Hoor Date: Mon, 4 Nov 2019 10:29:51 +0100 Subject: [PATCH 08/18] ADD: Libraries that were separated from the JDK in newer Java versions --- pom.xml | 21 +++++++++++++++++++ .../spring/resource/ExceptionHandlers.java | 12 +++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9f0cd4e..3d2bd51 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,27 @@ 5.3.7.Final + + javax.xml.bind + jaxb-api + 2.2.11 + + + com.sun.xml.bind + jaxb-core + 2.2.11 + + + com.sun.xml.bind + jaxb-impl + 2.2.11 + + + javax.activation + activation + 1.1.1 + + diff --git a/src/main/java/stenden/spring/resource/ExceptionHandlers.java b/src/main/java/stenden/spring/resource/ExceptionHandlers.java index 2c9cfe2..87fccb1 100644 --- a/src/main/java/stenden/spring/resource/ExceptionHandlers.java +++ b/src/main/java/stenden/spring/resource/ExceptionHandlers.java @@ -15,8 +15,16 @@ public class ExceptionHandlers { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(GreetingException.class) - public ErrorResponse handleGreetingException(GreetingException exception, HttpServletRequest request) { - return new ErrorResponse(String.format("I have the message '%s' for %s", exception.getMessage(), request.getRemoteAddr())); + public ErrorResponse handleGreetingException( + GreetingException exception, + HttpServletRequest request + ) { + String response = String.format( + "I have the message '%s' for %s", + exception.getMessage(), + request.getRemoteAddr() + ); + return new ErrorResponse(response); } } From 64146ef7fe433e1fc4d90b2723120540be67549e Mon Sep 17 00:00:00 2001 From: Sander ten Hoor Date: Tue, 26 Nov 2019 15:38:18 +0100 Subject: [PATCH 09/18] REF: Removed everything not-entitymanager related --- pom.xml | 12 ---- .../spring/configuration/DatabaseConfig.java | 51 ++--------------- .../stenden/spring/data/HouseRepository.java | 1 + .../data/hibernate/HibernateRepository.java | 32 ----------- .../data/jdbc/JdbcTemplateRepository.java | 38 ------------- .../spring/data/jdbc/PureJdbcRepository.java | 57 ------------------- .../data/jpa/EntityManagerJpaRepository.java | 19 +++---- .../spring/data/jpa/SpringJpaRepository.java | 7 --- .../spring/data/model/AnnotatedHouse.java | 6 +- .../stenden/spring/data/model/POJOHouse.java | 23 -------- .../spring/resource/HouseResource.java | 27 ++------- .../stenden/spring/service/HouseService.java | 36 ++---------- 12 files changed, 25 insertions(+), 284 deletions(-) delete mode 100644 src/main/java/stenden/spring/data/hibernate/HibernateRepository.java delete mode 100644 src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java delete mode 100644 src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java delete mode 100644 src/main/java/stenden/spring/data/jpa/SpringJpaRepository.java delete mode 100644 src/main/java/stenden/spring/data/model/POJOHouse.java diff --git a/pom.xml b/pom.xml index 3d2bd51..11f66f8 100644 --- a/pom.xml +++ b/pom.xml @@ -25,24 +25,12 @@ ${spring.version} - - org.springframework - spring-jdbc - ${spring.version} - - org.springframework spring-orm ${spring.version} - - org.springframework.data - spring-data-jpa - ${spring-data-jpa.version} - - javax.servlet diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index fa115ee..9cdfd1f 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -1,17 +1,13 @@ package stenden.spring.configuration; import lombok.extern.slf4j.Slf4j; -import org.hibernate.SessionFactory; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.orm.hibernate5.HibernateTransactionManager; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.Database; @@ -20,12 +16,10 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; -import java.util.Properties; @Slf4j @Configuration @EnableTransactionManagement // Required for Hibernate -@EnableJpaRepositories("stenden.spring.data") public class DatabaseConfig { /** @@ -45,49 +39,16 @@ public DataSource dataSource() { } /** - * JDBC by itself is tough to use, so we wrap it in the {@link JdbcTemplate} from Spring, - * which handles a lot of the boilerplate for us. - * Pure JDBC has its uses though. While it gives a lot of boilerplate, - * it also gives a lot of control, which can be useful for certain applications. - * Think of having to make very complex queries for very specific situations. + * Whhn just using JPA, you could also use this transaction manager. * - * @param dataSource * @return */ @Bean - public JdbcTemplate jdbcTemplate(DataSource dataSource) { - return new JdbcTemplate(dataSource); - } - - @Bean - public LocalSessionFactoryBean sessionFactory(DataSource dataSource) { - LocalSessionFactoryBean sfb = new LocalSessionFactoryBean(); - sfb.setDataSource(dataSource); - sfb.setPackagesToScan("stenden.spring.data.model"); - Properties props = new Properties(); - props.setProperty("dialect", "org.hibernate.dialect.H2Dialect"); - sfb.setHibernateProperties(props); - return sfb; - } - - @Bean - public PlatformTransactionManager transactionManager(SessionFactory sessionFactory) { - HibernateTransactionManager transactionManager = new HibernateTransactionManager(); - transactionManager.setSessionFactory(sessionFactory); - return transactionManager; + public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { + JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); + jpaTransactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject()); + return jpaTransactionManager; } - - /** - * Whhn just using JPA, you could also use this transaction manager. - * - * @return - */ -// @Bean -// public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { -// JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); -// jpaTransactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject()); -// return jpaTransactionManager; -// } @Bean public BeanPostProcessor persistenceTranslation() { return new PersistenceExceptionTranslationPostProcessor(); diff --git a/src/main/java/stenden/spring/data/HouseRepository.java b/src/main/java/stenden/spring/data/HouseRepository.java index 70c4d2d..342b86b 100644 --- a/src/main/java/stenden/spring/data/HouseRepository.java +++ b/src/main/java/stenden/spring/data/HouseRepository.java @@ -4,4 +4,5 @@ public interface HouseRepository { House getByID(Long id); + House addHouse(House house); } diff --git a/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java b/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java deleted file mode 100644 index af40618..0000000 --- a/src/main/java/stenden/spring/data/hibernate/HibernateRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -package stenden.spring.data.hibernate; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; -import stenden.spring.data.House; -import stenden.spring.data.HouseRepository; -import stenden.spring.data.model.AnnotatedHouse; - -import javax.transaction.Transactional; - -@Repository -public class HibernateRepository implements HouseRepository { - - private SessionFactory sessionFactory; - - @Autowired - public HibernateRepository(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } - - private Session currentSession() { - return sessionFactory.getCurrentSession(); - } - - @Transactional - public House getByID(Long id) { - return currentSession().get(AnnotatedHouse.class, id); - } - -} diff --git a/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java b/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java deleted file mode 100644 index 19c2e52..0000000 --- a/src/main/java/stenden/spring/data/jdbc/JdbcTemplateRepository.java +++ /dev/null @@ -1,38 +0,0 @@ -package stenden.spring.data.jdbc; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.stereotype.Repository; -import stenden.spring.data.House; -import stenden.spring.data.HouseRepository; -import stenden.spring.data.model.POJOHouse; - -import java.sql.ResultSet; -import java.sql.SQLException; - -@Repository -public class JdbcTemplateRepository implements HouseRepository { - - private static final String GET_HOUSE_BY_ID = - "SELECT ID, NR_OF_FLOORS, NR_OF_ROOMS, STREET, CITY FROM HOUSES WHERE ID = ?"; - - private JdbcOperations jdbcOperations; - - @Autowired - public JdbcTemplateRepository(JdbcOperations jdbcOperations) { - this.jdbcOperations = jdbcOperations; - } - - public House getByID(Long id) { - return jdbcOperations.queryForObject(GET_HOUSE_BY_ID, this::rowMapper, id); - } - - private POJOHouse rowMapper(ResultSet rs, int rowNum) throws SQLException { - return new POJOHouse(rs.getLong("ID"), - rs.getInt("NR_OF_FLOORS"), - rs.getInt("NR_OF_ROOMS"), - rs.getString("STREET"), - rs.getString("CITY")); - } - -} diff --git a/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java b/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java deleted file mode 100644 index 14da3e2..0000000 --- a/src/main/java/stenden/spring/data/jdbc/PureJdbcRepository.java +++ /dev/null @@ -1,57 +0,0 @@ -package stenden.spring.data.jdbc; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.UncategorizedSQLException; -import org.springframework.stereotype.Repository; -import stenden.spring.data.House; -import stenden.spring.data.HouseRepository; -import stenden.spring.data.model.POJOHouse; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -@Slf4j -@Repository -public class PureJdbcRepository implements HouseRepository { - - private static final String GET_HOUSE_BY_ID = - "SELECT ID, NR_OF_FLOORS, NR_OF_ROOMS, STREET, CITY FROM HOUSES WHERE ID = ?"; - - private DataSource dataSource; - - @Autowired - public PureJdbcRepository(DataSource dataSource) { - this.dataSource = dataSource; - } - - public House getByID(Long id) { - try ( - Connection connection = dataSource.getConnection(); - PreparedStatement statement = createPreparedStatement(connection, id); - ResultSet resultSet = statement.executeQuery() - ) { - resultSet.first(); - return new POJOHouse( - resultSet.getLong("ID"), - resultSet.getInt("NR_OF_FLOORS"), - resultSet.getInt("NR_OF_ROOMS"), - resultSet.getString("STREET"), - resultSet.getString("CITY") - ); - } catch (SQLException e) { - // Any kind of exception thrown is an SQLException. So it could be anything... - log.error("The query failed!", e); - throw new UncategorizedSQLException("Querying for a house", GET_HOUSE_BY_ID, e); - } - } - - private PreparedStatement createPreparedStatement(Connection connection, Long id) throws SQLException { - PreparedStatement statement = connection.prepareStatement(GET_HOUSE_BY_ID); - statement.setLong(1, id); - return statement; - } -} diff --git a/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java b/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java index 5775898..b8d3005 100644 --- a/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java +++ b/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java @@ -9,10 +9,6 @@ import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.ParameterExpression; -import javax.persistence.criteria.Root; import javax.transaction.Transactional; @Repository @@ -31,14 +27,13 @@ public class EntityManagerJpaRepository implements HouseRepository { @Override @Transactional public House getByID(Long id) { - CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder(); - CriteriaQuery query = criteriaBuilder.createQuery(AnnotatedHouse.class); - Root from = query.from(AnnotatedHouse.class); - ParameterExpression parameter = criteriaBuilder.parameter(Long.class); - query.select(from).where(criteriaBuilder.equal(from.get("id"), parameter)); + return em.find(AnnotatedHouse.class, id); + } - return em.createQuery(query).setParameter(parameter, id).getSingleResult(); - // A simpler way of doing it -// return em.find(AnnotatedHouse.class, id); + @Override + @Transactional + public House addHouse(House house) { + em.persist(house); + return house; } } diff --git a/src/main/java/stenden/spring/data/jpa/SpringJpaRepository.java b/src/main/java/stenden/spring/data/jpa/SpringJpaRepository.java deleted file mode 100644 index 3b69e8e..0000000 --- a/src/main/java/stenden/spring/data/jpa/SpringJpaRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package stenden.spring.data.jpa; - -import org.springframework.data.jpa.repository.JpaRepository; -import stenden.spring.data.model.AnnotatedHouse; - -public interface SpringJpaRepository extends JpaRepository { -} diff --git a/src/main/java/stenden/spring/data/model/AnnotatedHouse.java b/src/main/java/stenden/spring/data/model/AnnotatedHouse.java index 6072540..8e0e3b1 100644 --- a/src/main/java/stenden/spring/data/model/AnnotatedHouse.java +++ b/src/main/java/stenden/spring/data/model/AnnotatedHouse.java @@ -6,10 +6,7 @@ import lombok.NoArgsConstructor; import stenden.spring.data.House; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; +import javax.persistence.*; @Data @AllArgsConstructor @@ -19,6 +16,7 @@ public class AnnotatedHouse implements House { @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "NR_OF_FLOORS") diff --git a/src/main/java/stenden/spring/data/model/POJOHouse.java b/src/main/java/stenden/spring/data/model/POJOHouse.java deleted file mode 100644 index 0e5acff..0000000 --- a/src/main/java/stenden/spring/data/model/POJOHouse.java +++ /dev/null @@ -1,23 +0,0 @@ -package stenden.spring.data.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import stenden.spring.data.House; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class POJOHouse implements House { - - private Long id; - - private Integer nrOfFloors; - - private Integer nrOfRooms; - - private String street; - - private String city; - -} diff --git a/src/main/java/stenden/spring/resource/HouseResource.java b/src/main/java/stenden/spring/resource/HouseResource.java index 0cd5aae..82898c4 100644 --- a/src/main/java/stenden/spring/resource/HouseResource.java +++ b/src/main/java/stenden/spring/resource/HouseResource.java @@ -1,11 +1,9 @@ package stenden.spring.resource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import stenden.spring.data.House; +import stenden.spring.data.model.AnnotatedHouse; import stenden.spring.service.HouseService; // We're telling Spring MVC that this is a REST controller @@ -23,29 +21,14 @@ public HouseResource(HouseService houseService) { this.houseService = houseService; } - @GetMapping("/templatejdbc/{id}") - public House getTemplateJdbcHouse(@PathVariable("id") Long id) { - return houseService.getTemplateJdbcHouse(id); - } - - @GetMapping("/purejdbc/{id}") - public House getPureJdbcHouse(@PathVariable("id") Long id) { - return houseService.getPureJdbcHouse(id); - } - - @GetMapping("/hibernate/{id}") - public House getHibernateHouse(@PathVariable("id") Long id) { - return houseService.getHibernateHouse(id); - } - @GetMapping("/jpa/{id}") public House getJpaHouse(@PathVariable("id") Long id) { return houseService.getJpaHouse(id); } - @GetMapping("/springjpa/{id}") - public House getSpringJpaHouse(@PathVariable("id") Long id) { - return houseService.getSpringJpaHouse(id); + @PostMapping + public House postJpaHouse(@RequestBody AnnotatedHouse house) { + return houseService.addHouse(house); } } diff --git a/src/main/java/stenden/spring/service/HouseService.java b/src/main/java/stenden/spring/service/HouseService.java index 37d2f91..e248b89 100644 --- a/src/main/java/stenden/spring/service/HouseService.java +++ b/src/main/java/stenden/spring/service/HouseService.java @@ -4,52 +4,24 @@ import org.springframework.stereotype.Service; import stenden.spring.data.House; import stenden.spring.data.HouseRepository; -import stenden.spring.data.jpa.SpringJpaRepository; - -import javax.persistence.EntityNotFoundException; +import stenden.spring.data.model.AnnotatedHouse; @Service public class HouseService { - private final HouseRepository jdbcTemplateRepository; - private final HouseRepository pureJdbcRepository; - private final HouseRepository hibernateRepository; private final HouseRepository entityManagerJpaRepository; - private final SpringJpaRepository springJpaRepository; @Autowired - public HouseService(HouseRepository jdbcTemplateRepository, - HouseRepository pureJdbcRepository, - HouseRepository hibernateRepository, - HouseRepository entityManagerJpaRepository, - SpringJpaRepository springJpaRepository) { - this.jdbcTemplateRepository = jdbcTemplateRepository; - this.pureJdbcRepository = pureJdbcRepository; - this.hibernateRepository = hibernateRepository; + public HouseService(HouseRepository entityManagerJpaRepository) { this.entityManagerJpaRepository = entityManagerJpaRepository; - this.springJpaRepository = springJpaRepository; - } - - public House getTemplateJdbcHouse(Long id) { - return jdbcTemplateRepository.getByID(id); - } - - public House getPureJdbcHouse(Long id) { - return pureJdbcRepository.getByID(id); - } - - public House getHibernateHouse(Long id) { - return hibernateRepository.getByID(id); } public House getJpaHouse(Long id) { return entityManagerJpaRepository.getByID(id); } - public House getSpringJpaHouse(Long id) { - return springJpaRepository.findById(id) - .orElseThrow(() -> new EntityNotFoundException("House with ID " + id + " not found")); + public House addHouse(AnnotatedHouse house) { + return entityManagerJpaRepository.addHouse(house); } - } From 8e8ce8d0e8988dd772ea64b46463a37b88fbc274 Mon Sep 17 00:00:00 2001 From: Sander ten Hoor Date: Thu, 5 Dec 2019 16:58:45 +0100 Subject: [PATCH 10/18] ADD: REST security ADD: A USERS table --- pom.xml | 19 ++++ .../spring/configuration/SecurityConfig.java | 96 +++++++++++++++++++ .../SpringSecurityInitializer.java | 6 ++ .../spring/security/HTTPStatusHandler.java | 24 +++++ .../stenden/spring/security/JWTFilter.java | 49 ++++++++++ .../stenden/spring/security/JWTProvider.java | 69 +++++++++++++ .../spring/security/JWTStatusHandler.java | 26 +++++ .../security/RestAccessDeniedHandler.java | 23 +++++ .../RestAuthenticationEntryPoint.java | 22 +++++ src/main/resources/application.properties | 3 +- src/main/resources/insert-data.sql | 5 +- src/main/resources/schema.sql | 22 +++-- 12 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 src/main/java/stenden/spring/configuration/SecurityConfig.java create mode 100644 src/main/java/stenden/spring/configuration/SpringSecurityInitializer.java create mode 100644 src/main/java/stenden/spring/security/HTTPStatusHandler.java create mode 100644 src/main/java/stenden/spring/security/JWTFilter.java create mode 100644 src/main/java/stenden/spring/security/JWTProvider.java create mode 100644 src/main/java/stenden/spring/security/JWTStatusHandler.java create mode 100755 src/main/java/stenden/spring/security/RestAccessDeniedHandler.java create mode 100755 src/main/java/stenden/spring/security/RestAuthenticationEntryPoint.java diff --git a/pom.xml b/pom.xml index 11f66f8..c170039 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,18 @@ ${spring.version} + + org.springframework.security + spring-security-web + ${spring.version} + + + + org.springframework.security + spring-security-config + ${spring.version} + + javax.servlet @@ -38,6 +50,13 @@ 4.0.1 + + + io.jsonwebtoken + jjwt + 0.9.1 + + com.fasterxml.jackson.core diff --git a/src/main/java/stenden/spring/configuration/SecurityConfig.java b/src/main/java/stenden/spring/configuration/SecurityConfig.java new file mode 100644 index 0000000..4eba9d0 --- /dev/null +++ b/src/main/java/stenden/spring/configuration/SecurityConfig.java @@ -0,0 +1,96 @@ +package stenden.spring.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.JdbcUserDetailsManager; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import stenden.spring.security.*; + +import javax.sql.DataSource; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + DataSource dataSource; + @Autowired + private RestAuthenticationEntryPoint restAuthenticationEntryPoint; + @Autowired + private RestAccessDeniedHandler restAccessDeniedHandler; + @Autowired + private JWTProvider jwtProvider; + + @Bean + public PasswordEncoder encoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + @Override + public UserDetailsService userDetailsServiceBean() throws Exception { + return super.userDetailsServiceBean(); + } + + @Bean + public JWTProvider jwtProvider(UserDetailsService userDetailsService, @Value("secret.key") String secretKey) { + return new JWTProvider(userDetailsService, secretKey); + } + + @Override + public void configure(AuthenticationManagerBuilder auth) throws Exception { + JdbcUserDetailsManager userDetailsService = auth.jdbcAuthentication() + .dataSource(dataSource) + .getUserDetailsService(); + + userDetailsService + .setUsersByUsernameQuery("SELECT USERNAME, PASSWORD, ENABLED FROM USERS WHERE USERNAME = ?"); + + userDetailsService + .setAuthoritiesByUsernameQuery("SELECT USERNAME, ROLE FROM USERS WHERE USERNAME = ?"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + enableRESTAuthentication(http) + .authorizeRequests() + .anyRequest() + .hasRole("USER") + .and() + .csrf().disable(); + } + + private HttpSecurity enableRESTAuthentication(HttpSecurity http) throws Exception { + http + .formLogin().permitAll() + // If login fails, return 401 + .failureHandler(new HTTPStatusHandler(HttpStatus.UNAUTHORIZED)) + // If login succeeds return 200 + .successHandler(new JWTStatusHandler(jwtProvider)) + .and() + .exceptionHandling() + .accessDeniedHandler(restAccessDeniedHandler) + .authenticationEntryPoint(restAuthenticationEntryPoint) + .and() + .httpBasic(); + + http.addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + return http; + } + + +} diff --git a/src/main/java/stenden/spring/configuration/SpringSecurityInitializer.java b/src/main/java/stenden/spring/configuration/SpringSecurityInitializer.java new file mode 100644 index 0000000..9a8784d --- /dev/null +++ b/src/main/java/stenden/spring/configuration/SpringSecurityInitializer.java @@ -0,0 +1,6 @@ +package stenden.spring.configuration; + +import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; + +public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer { +} diff --git a/src/main/java/stenden/spring/security/HTTPStatusHandler.java b/src/main/java/stenden/spring/security/HTTPStatusHandler.java new file mode 100644 index 0000000..f4deb87 --- /dev/null +++ b/src/main/java/stenden/spring/security/HTTPStatusHandler.java @@ -0,0 +1,24 @@ +package stenden.spring.security; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class HTTPStatusHandler implements AuthenticationFailureHandler { + + private HttpStatus status; + + public HTTPStatusHandler(HttpStatus status) { + this.status = status; + } + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) { + response.setStatus(status.value()); + } + +} diff --git a/src/main/java/stenden/spring/security/JWTFilter.java b/src/main/java/stenden/spring/security/JWTFilter.java new file mode 100644 index 0000000..515b59c --- /dev/null +++ b/src/main/java/stenden/spring/security/JWTFilter.java @@ -0,0 +1,49 @@ +package stenden.spring.security; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class JWTFilter extends GenericFilterBean { + + private JWTProvider jwtProvider; + + public JWTFilter(JWTProvider jwtProvider) { + this.jwtProvider = jwtProvider; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + // Get the token from the request + String token = jwtProvider.getToken((HttpServletRequest) request); + try { + // If there was a token and it is valid (e.g.: not expired) + if (token != null && jwtProvider.validateToken(token)) { + // Get the authentication instance and set it in the SecurityContext + SecurityContextHolder.getContext().setAuthentication(jwtProvider.getAuthentication(token)); + + // Check if the token is about to expire and we need to generate a fresh one + String newToken = jwtProvider.getRefreshToken(token); + if (newToken != null) { + // In that case we will return it on a different header + ((HttpServletResponse) response).addHeader("jwt-new-token", newToken); + } + } + } catch (Exception e) { + // Be sure to clear everything if something when wrong + SecurityContextHolder.clearContext(); + } + + // Let the filter chain go on + chain.doFilter(request, response); + } + +} diff --git a/src/main/java/stenden/spring/security/JWTProvider.java b/src/main/java/stenden/spring/security/JWTProvider.java new file mode 100644 index 0000000..6c8272a --- /dev/null +++ b/src/main/java/stenden/spring/security/JWTProvider.java @@ -0,0 +1,69 @@ +package stenden.spring.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; + +import javax.servlet.http.HttpServletRequest; +import java.util.Base64; +import java.util.Date; + +public class JWTProvider { + + private static final SignatureAlgorithm ALGORITHM = SignatureAlgorithm.HS256; + private final UserDetailsService myUserDetails; + private String secretKey; + private long validityInMilliseconds = 600000; // 1minute + + public JWTProvider(UserDetailsService myUserDetails, String secretKey) { + this.myUserDetails = myUserDetails; + this.secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + public String createToken(String username) { + Date now = new Date(); + Date expiration = new Date(now.getTime() + validityInMilliseconds); + return Jwts.builder()// + .setSubject(username)// + .setIssuedAt(now)// + .setExpiration(expiration)// + .signWith(ALGORITHM, secretKey)// + .compact(); + } + + public Authentication getAuthentication(String tokenString) { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(tokenString); + String user = claims.getBody().getSubject(); + UserDetails userDetails = myUserDetails.loadUserByUsername(user); + return new UsernamePasswordAuthenticationToken(userDetails, "", + userDetails.getAuthorities()); + } + + public String getRefreshToken(String tokenString) { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(tokenString); + String user = claims.getBody().getSubject(); + Date expiration = claims.getBody().getExpiration(); + if (new Date(new Date().getTime() + validityInMilliseconds / 10).after(expiration)) { + return createToken(user); + } + return null; + } + + public String getToken(HttpServletRequest req) { + String bearerToken = req.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + public boolean validateToken(String token) { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return SignatureAlgorithm.forName(claims.getHeader().getAlgorithm()) == ALGORITHM; + } +} diff --git a/src/main/java/stenden/spring/security/JWTStatusHandler.java b/src/main/java/stenden/spring/security/JWTStatusHandler.java new file mode 100644 index 0000000..ae7a5ba --- /dev/null +++ b/src/main/java/stenden/spring/security/JWTStatusHandler.java @@ -0,0 +1,26 @@ +package stenden.spring.security; + +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class JWTStatusHandler implements AuthenticationSuccessHandler { + + private final JWTProvider jwtProvider; + + public JWTStatusHandler(JWTProvider jwtProvider) { + this.jwtProvider = jwtProvider; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) { + String token = jwtProvider.createToken(authentication.getName()); + response.addHeader("jwt-token", token); + response.setStatus(HttpStatus.OK.value()); + } + +} diff --git a/src/main/java/stenden/spring/security/RestAccessDeniedHandler.java b/src/main/java/stenden/spring/security/RestAccessDeniedHandler.java new file mode 100755 index 0000000..2e634ee --- /dev/null +++ b/src/main/java/stenden/spring/security/RestAccessDeniedHandler.java @@ -0,0 +1,23 @@ +package stenden.spring.security; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class RestAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + response.setStatus(403); + response.getWriter().write("{\"message\":\"You\'re not allowed in here.\"}"); + + } +} diff --git a/src/main/java/stenden/spring/security/RestAuthenticationEntryPoint.java b/src/main/java/stenden/spring/security/RestAuthenticationEntryPoint.java new file mode 100755 index 0000000..5da2f68 --- /dev/null +++ b/src/main/java/stenden/spring/security/RestAuthenticationEntryPoint.java @@ -0,0 +1,22 @@ +package stenden.spring.security; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence( + HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse, + AuthenticationException e) throws IOException, ServletException { + httpServletResponse.setStatus(401); + httpServletResponse.getWriter().write("{\"message\":\"You're not authorized.\"}"); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3919f30..0f6346e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ -stenden.greeting=Hello Stenden Students! \ No newline at end of file +stenden.greeting=Hello Stenden Students! +secret.key=secret \ No newline at end of file diff --git a/src/main/resources/insert-data.sql b/src/main/resources/insert-data.sql index 5187fcc..97e2a2f 100644 --- a/src/main/resources/insert-data.sql +++ b/src/main/resources/insert-data.sql @@ -1,2 +1,5 @@ INSERT INTO HOUSES (NR_OF_FLOORS, NR_OF_ROOMS, STREET, CITY) -VALUES (4, 12, 'Ubbo Emmiussingel 112', 'Groningen'); \ No newline at end of file +VALUES (4, 12, 'Ubbo Emmiussingel 112', 'Groningen'); + +INSERT INTO USERS (USERNAME, PASSWORD, ENABLED, ROLE) +VALUES ('user', '$2a$10$gLqaddEO8/NDnDDuwyhaEePlq9DK4bb/xYyyxyg94lk7gCysbkz8K', true, 'ROLE_USER'); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 53ae414..27a1b44 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,8 +1,18 @@ -CREATE TABLE HOUSES ( - ID int NOT NULL AUTO_INCREMENT, +CREATE TABLE HOUSES +( + ID int NOT NULL AUTO_INCREMENT, NR_OF_FLOORS NUMBER, - NR_OF_ROOMS NUMBER, - STREET varchar(255) NOT NULL, - CITY varchar(255) NOT NULL, + NR_OF_ROOMS NUMBER, + STREET varchar(255) NOT NULL, + CITY varchar(255) NOT NULL, PRIMARY KEY (ID) -); \ No newline at end of file +); + +CREATE TABLE USERS +( + ID int NOT NULL AUTO_INCREMENT, + USERNAME varchar(30) NOT NULL UNIQUE, + PASSWORD varchar(64) NOT NULL, + ENABLED bool NOT NULL, + ROLE varchar(20) NOT NULL +) \ No newline at end of file From fa80689852891c53eea6807701000d4d9deba197 Mon Sep 17 00:00:00 2001 From: Sander ten Hoor Date: Fri, 6 Dec 2019 18:14:19 +0100 Subject: [PATCH 11/18] ADD: Documentation --- pom.xml | 6 + .../configuration/ApplicationInitializer.java | 60 +++++----- .../spring/configuration/DatabaseConfig.java | 91 +++++++------- .../spring/configuration/SecurityConfig.java | 111 ++++++++++++++++-- .../SpringSecurityInitializer.java | 15 +++ src/main/java/stenden/spring/data/House.java | 20 ++-- .../stenden/spring/data/HouseRepository.java | 4 +- .../data/jpa/EntityManagerJpaRepository.java | 36 +++--- .../spring/data/model/AnnotatedHouse.java | 22 ++-- .../spring/resource/ErrorResponse.java | 2 +- .../spring/resource/ExceptionHandlers.java | 26 ++-- .../spring/resource/GreetingException.java | 6 +- .../spring/resource/HelloWorldResource.java | 62 +++++----- .../spring/resource/HouseResource.java | 26 ++-- .../java/stenden/spring/resource/Message.java | 2 +- .../stenden/spring/security/JWTFilter.java | 13 ++ .../stenden/spring/security/JWTProvider.java | 79 +++++++++++-- .../spring/security/JWTStatusHandler.java | 11 ++ .../stenden/spring/service/HouseService.java | 22 ++-- 19 files changed, 403 insertions(+), 211 deletions(-) diff --git a/pom.xml b/pom.xml index c170039..58bf73a 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,12 @@ 1.1.1 + + org.mariadb.jdbc + mariadb-java-client + 2.5.2 + + diff --git a/src/main/java/stenden/spring/configuration/ApplicationInitializer.java b/src/main/java/stenden/spring/configuration/ApplicationInitializer.java index 67f5b53..daeaff3 100644 --- a/src/main/java/stenden/spring/configuration/ApplicationInitializer.java +++ b/src/main/java/stenden/spring/configuration/ApplicationInitializer.java @@ -7,36 +7,36 @@ */ public class ApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - /** - * We accept all incoming requests starting at / - * - * @return All the mappings we accept - */ - @Override - protected String[] getServletMappings() { - return new String[]{"/"}; - } + /** + * We accept all incoming requests starting at / + * + * @return All the mappings we accept + */ + @Override + protected String[] getServletMappings() { + return new String[]{"/"}; + } - /** - * The classes we use for configuring our ApplicationContext - * - * @return An array containing configuration classes for our ApplicationContext - */ - @Override - protected Class[] getRootConfigClasses() { - return new Class[]{WebConfig.class}; - } + /** + * The classes we use for configuring our ApplicationContext + * + * @return An array containing configuration classes for our ApplicationContext + */ + @Override + protected Class[] getRootConfigClasses() { + return new Class[]{WebConfig.class}; + } - /** - * We have no special DispatcherServlet logic, so we do all the configuration in {@link WebConfig} - * It is useful if you have multiple DispatcherServlets and you want to specifically manage the different - * WebApplicationContexts - * https://stackoverflow.com/questions/35258758/getservletconfigclasses-vs-getrootconfigclasses-when-extending-abstractannot - * - * @return null, as it's not necessary for us. - */ - @Override - protected Class[] getServletConfigClasses() { - return null; - } + /** + * We have no special DispatcherServlet logic, so we do all the configuration in {@link WebConfig} + * It is useful if you have multiple DispatcherServlets and you want to specifically manage the different + * WebApplicationContexts + * https://stackoverflow.com/questions/35258758/getservletconfigclasses-vs-getrootconfigclasses-when-extending-abstractannot + * + * @return null, as it's not necessary for us. + */ + @Override + protected Class[] getServletConfigClasses() { + return null; + } } diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index 9cdfd1f..eed8bdc 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -1,12 +1,11 @@ package stenden.spring.configuration; import lombok.extern.slf4j.Slf4j; +import org.mariadb.jdbc.MariaDbPoolDataSource; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -16,57 +15,57 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; +import java.sql.SQLException; @Slf4j @Configuration @EnableTransactionManagement // Required for Hibernate public class DatabaseConfig { - /** - * The {@link DataSource} representing the database connection. - * In our case we're creating an in-memory database using H2, - * so the setup is simple. - * - * @return The connection to the database - */ - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.H2) - .addScript("classpath:schema.sql") - .addScript("classpath:insert-data.sql") - .build(); - } + /** + * The {@link DataSource} representing the database connection. + * + * @return The connection to the database + */ + @Bean + public DataSource dataSource() throws SQLException { + MariaDbPoolDataSource dataSource = new MariaDbPoolDataSource(); + dataSource.setUser("root"); + dataSource.setPassword("root"); + dataSource.setUrl("jdbc:mariadb://localhost:3306/example"); + return dataSource; + } - /** - * Whhn just using JPA, you could also use this transaction manager. - * - * @return - */ - @Bean - public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { - JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); - jpaTransactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject()); - return jpaTransactionManager; - } - @Bean - public BeanPostProcessor persistenceTranslation() { - return new PersistenceExceptionTranslationPostProcessor(); - } + /** + * Whhn just using JPA, you could also use this transaction manager. + * + * @return + */ + @Bean + public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) { + JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); + jpaTransactionManager.setEntityManagerFactory(entityManagerFactoryBean.getObject()); + return jpaTransactionManager; + } - @Bean - public JpaVendorAdapter jpaVendorAdapter() { - HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter(); - adapter.setDatabase(Database.H2); - return adapter; - } + @Bean + public BeanPostProcessor persistenceTranslation() { + return new PersistenceExceptionTranslationPostProcessor(); + } - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - entityManagerFactoryBean.setDataSource(dataSource); - entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter); - entityManagerFactoryBean.setPackagesToScan("stenden.spring.data.model"); - return entityManagerFactoryBean; - } + @Bean + public JpaVendorAdapter jpaVendorAdapter() { + HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter(); + adapter.setDatabase(Database.H2); + return adapter; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) { + LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + entityManagerFactoryBean.setDataSource(dataSource); + entityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter); + entityManagerFactoryBean.setPackagesToScan("stenden.spring.data.model"); + return entityManagerFactoryBean; + } } diff --git a/src/main/java/stenden/spring/configuration/SecurityConfig.java b/src/main/java/stenden/spring/configuration/SecurityConfig.java index 4eba9d0..fa0b01b 100644 --- a/src/main/java/stenden/spring/configuration/SecurityConfig.java +++ b/src/main/java/stenden/spring/configuration/SecurityConfig.java @@ -7,8 +7,10 @@ import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; 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.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; @@ -20,13 +22,21 @@ import javax.sql.DataSource; +/** + * The annotation @{@link EnableWebSecurity} triggers Spring to load the {@link WebSecurityConfiguration}, + * which for one thing exports the springSecurityFilterChainBean, which the filter registered by {@link SecurityConfig} + * delegates to. + * That filter looks for classes implementing {@link WebSecurityConfigurer}. + * {@link WebSecurityConfigurerAdapter} is a subclass of {@link WebSecurityConfigurer} and thus this class is as well. + * Then this class gets used for configuring the security of your application. + */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired - DataSource dataSource; + private DataSource dataSource; @Autowired private RestAuthenticationEntryPoint restAuthenticationEntryPoint; @Autowired @@ -34,63 +44,148 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JWTProvider jwtProvider; + /** + * Here we define what we want to use for password encoding. + * + * @return The desired encoder implementation. + */ @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } + /** + * To expose an UserDetailService in our application, + * we can just override this method from the superclass. + *

+ * We can also create our own UserDetailService if we so desire, + * to customize how users are retrieved! + * + * @return The UserDetailService + * @throws Exception + */ @Bean @Override public UserDetailsService userDetailsServiceBean() throws Exception { return super.userDetailsServiceBean(); } + /** + * We create the JWTProvider bean, which + * + * @param userDetailsService + * @param secretKey + * @return + */ @Bean public JWTProvider jwtProvider(UserDetailsService userDetailsService, @Value("secret.key") String secretKey) { return new JWTProvider(userDetailsService, secretKey); } + /** + * We're configuring the {@link AuthenticationManagerBuilder} to tell it where to get the users from. + * By default Spring expects certain tables to be present if you simpy use {@link AuthenticationManagerBuilder#jdbcAuthentication()} + * but you can customize that by giving your own queries, as shown in the method. + * You still need to make sure the query returns the right columns. + * You can also implement your own {@link UserDetailsService} where you'll retrieve users instead. + * + * @param auth + * @throws Exception + */ @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { JdbcUserDetailsManager userDetailsService = auth.jdbcAuthentication() .dataSource(dataSource) .getUserDetailsService(); + // Spring requires USERNAME, PASSWORD and ENABLED userDetailsService .setUsersByUsernameQuery("SELECT USERNAME, PASSWORD, ENABLED FROM USERS WHERE USERNAME = ?"); + // Here Spring requires USERNAME and ROLE userDetailsService .setAuthoritiesByUsernameQuery("SELECT USERNAME, ROLE FROM USERS WHERE USERNAME = ?"); } + /** + * In this method we get a {@link HttpSecurity} object we can use for configuring the security in our application. + *

+ * It's important to know that the top most configuration should be more specific than the bottom configuration. + * If I say the url /admin is accessible by admins only and then say all the URLS are open for everyone, + * that is interpreted as "/admin is secured, but everything else is open for business". + * However, if I were to do that the other way around, my rule for /admin would be ignored. + * + * @param http The object we configure our security in + * @throws Exception + */ @Override protected void configure(HttpSecurity http) throws Exception { + // First we configure it to allow authentication and authorization in REST enableRESTAuthentication(http) + // Now let's say which requests we want to authorize .authorizeRequests() + // Every single request needs to be authorized... with the exception of /login, + // which was defined in enableRESTAuthentication(), which was earlier and thus trumps this configuration. .anyRequest() + // We require every user to have the role USER (this translates to 'ROLE_USER' in the database + // and in other places) .hasRole("USER") + // Alright, we're done, let's move on to the next part using and() .and() - .csrf().disable(); + // We're disabling defenses against Cross-Site Request Forgery, + // as the browser is not responsible for adding authentication information to the request + // which is wat the CSRF exploit relies on. + .csrf() + .disable(); } + /** + * I separated the configuration for security in REST to simplify it. + * In this method we enable logging in and configure it for a REST API. + * + * @param http The {@link HttpSecurity} object we can use for configuration + * @return + * @throws Exception + */ private HttpSecurity enableRESTAuthentication(HttpSecurity http) throws Exception { http - .formLogin().permitAll() - // If login fails, return 401 + // We'll allow logging in as if it were a form, using form headers + .formLogin() + // Everyone is allowed to reach this endpoint + .permitAll() + // If login fails, return a 401 instead of redirecting, as is normal for form login .failureHandler(new HTTPStatusHandler(HttpStatus.UNAUTHORIZED)) - // If login succeeds return 200 + // If login succeeds return a 200 instead of redirecting, as is normal for form login + // We also return a JSON Web Token .successHandler(new JWTStatusHandler(jwtProvider)) + // Configuring the HttpSecurity object is done in steps, we just configured the formLogin, + // now we're continuing to exception handling. To signal this, we need to use and(), + // otherwise we'd still be trying to configure formLogin. .and() + // What to do when it goes wrong? .exceptionHandling() + // When the access is denied to a certain part of the application, use the given handler. .accessDeniedHandler(restAccessDeniedHandler) - .authenticationEntryPoint(restAuthenticationEntryPoint) - .and() - .httpBasic(); + // When the authentication fails, use the given handler. + .authenticationEntryPoint(restAuthenticationEntryPoint); + // We need to register our JWTFilter. We register it before the UsernamePasswordAuthenticationFilter, + // as that is part of the group of filters where Spring expects updates to the SecurityContextHolder http.addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class); + + // As it's a REST API, we don't want Spring remembering sessions for users. It should be stateless. http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + // We return it so we can chain more configuration return http; } } + + + + + + + diff --git a/src/main/java/stenden/spring/configuration/SpringSecurityInitializer.java b/src/main/java/stenden/spring/configuration/SpringSecurityInitializer.java index 9a8784d..1318530 100644 --- a/src/main/java/stenden/spring/configuration/SpringSecurityInitializer.java +++ b/src/main/java/stenden/spring/configuration/SpringSecurityInitializer.java @@ -1,6 +1,21 @@ package stenden.spring.configuration; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.filter.DelegatingFilterProxy; +/** + * In the same manner as {@link ApplicationInitializer}, + * this class also implements {@link WebApplicationInitializer}. + * This means it is automatically found by the Web Container. + *

+ * Whereas {@link ApplicationInitializer} is for initializing the Spring Dispatcher Servlet and the Application Context, + * this one is for registering a filter, more specifically an instance of {@link DelegatingFilterProxy} called + * the springSecurityFilterChain. This acts as a proxy filter + */ public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer { + + // We don't need to implement anything, the superclass is complete. The only reason is left abstract is + // so it isn't picked up by the Web Container. + } diff --git a/src/main/java/stenden/spring/data/House.java b/src/main/java/stenden/spring/data/House.java index b6f538a..b21acab 100644 --- a/src/main/java/stenden/spring/data/House.java +++ b/src/main/java/stenden/spring/data/House.java @@ -2,24 +2,24 @@ public interface House { - Long getId(); + Long getId(); - void setId(Long id); + void setId(Long id); - Integer getNrOfFloors(); + Integer getNrOfFloors(); - void setNrOfFloors(Integer nrOfFloors); + void setNrOfFloors(Integer nrOfFloors); - Integer getNrOfRooms(); + Integer getNrOfRooms(); - void setNrOfRooms(Integer nrOfRooms); + void setNrOfRooms(Integer nrOfRooms); - String getStreet(); + String getStreet(); - void setStreet(String street); + void setStreet(String street); - String getCity(); + String getCity(); - void setCity(String city); + void setCity(String city); } diff --git a/src/main/java/stenden/spring/data/HouseRepository.java b/src/main/java/stenden/spring/data/HouseRepository.java index 342b86b..0e9f085 100644 --- a/src/main/java/stenden/spring/data/HouseRepository.java +++ b/src/main/java/stenden/spring/data/HouseRepository.java @@ -2,7 +2,7 @@ public interface HouseRepository { - House getByID(Long id); + House getByID(Long id); - House addHouse(House house); + House addHouse(House house); } diff --git a/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java b/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java index b8d3005..deaf51b 100644 --- a/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java +++ b/src/main/java/stenden/spring/data/jpa/EntityManagerJpaRepository.java @@ -16,24 +16,24 @@ @NoArgsConstructor public class EntityManagerJpaRepository implements HouseRepository { - /** - * This gives us an EntityManager. - * Or, well, a proxy to one. Which gives or creates a thread-safe EntityManager for us - * every time we use it. - */ - @PersistenceContext(unitName = "entityManagerFactory") - private EntityManager em; + /** + * This gives us an EntityManager. + * Or, well, a proxy to one. Which gives or creates a thread-safe EntityManager for us + * every time we use it. + */ + @PersistenceContext(unitName = "entityManagerFactory") + private EntityManager em; - @Override - @Transactional - public House getByID(Long id) { - return em.find(AnnotatedHouse.class, id); - } + @Override + @Transactional + public House getByID(Long id) { + return em.find(AnnotatedHouse.class, id); + } - @Override - @Transactional - public House addHouse(House house) { - em.persist(house); - return house; - } + @Override + @Transactional + public House addHouse(House house) { + em.persist(house); + return house; + } } diff --git a/src/main/java/stenden/spring/data/model/AnnotatedHouse.java b/src/main/java/stenden/spring/data/model/AnnotatedHouse.java index 8e0e3b1..8885fef 100644 --- a/src/main/java/stenden/spring/data/model/AnnotatedHouse.java +++ b/src/main/java/stenden/spring/data/model/AnnotatedHouse.java @@ -15,20 +15,20 @@ @Entity public class AnnotatedHouse implements House { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; - @Column(name = "NR_OF_FLOORS") - private Integer nrOfFloors; + @Column(name = "NR_OF_FLOORS") + private Integer nrOfFloors; - @Column(name = "NR_OF_ROOMS") - private Integer nrOfRooms; + @Column(name = "NR_OF_ROOMS") + private Integer nrOfRooms; - @Column(name = "STREET") - private String street; + @Column(name = "STREET") + private String street; - @Column(name = "CITY") - private String city; + @Column(name = "CITY") + private String city; } diff --git a/src/main/java/stenden/spring/resource/ErrorResponse.java b/src/main/java/stenden/spring/resource/ErrorResponse.java index cf12d5c..85b8d67 100644 --- a/src/main/java/stenden/spring/resource/ErrorResponse.java +++ b/src/main/java/stenden/spring/resource/ErrorResponse.java @@ -7,6 +7,6 @@ @AllArgsConstructor public class ErrorResponse { - private String error; + private String error; } diff --git a/src/main/java/stenden/spring/resource/ExceptionHandlers.java b/src/main/java/stenden/spring/resource/ExceptionHandlers.java index 87fccb1..364a423 100644 --- a/src/main/java/stenden/spring/resource/ExceptionHandlers.java +++ b/src/main/java/stenden/spring/resource/ExceptionHandlers.java @@ -13,18 +13,18 @@ @RestControllerAdvice public class ExceptionHandlers { - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(GreetingException.class) - public ErrorResponse handleGreetingException( - GreetingException exception, - HttpServletRequest request - ) { - String response = String.format( - "I have the message '%s' for %s", - exception.getMessage(), - request.getRemoteAddr() - ); - return new ErrorResponse(response); - } + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(GreetingException.class) + public ErrorResponse handleGreetingException( + GreetingException exception, + HttpServletRequest request + ) { + String response = String.format( + "I have the message '%s' for %s", + exception.getMessage(), + request.getRemoteAddr() + ); + return new ErrorResponse(response); + } } diff --git a/src/main/java/stenden/spring/resource/GreetingException.java b/src/main/java/stenden/spring/resource/GreetingException.java index f43f9de..306eda0 100644 --- a/src/main/java/stenden/spring/resource/GreetingException.java +++ b/src/main/java/stenden/spring/resource/GreetingException.java @@ -2,8 +2,8 @@ public class GreetingException extends RuntimeException { - public GreetingException(String message) { - super(message); - } + public GreetingException(String message) { + super(message); + } } diff --git a/src/main/java/stenden/spring/resource/HelloWorldResource.java b/src/main/java/stenden/spring/resource/HelloWorldResource.java index bbd3aca..a740350 100644 --- a/src/main/java/stenden/spring/resource/HelloWorldResource.java +++ b/src/main/java/stenden/spring/resource/HelloWorldResource.java @@ -14,39 +14,39 @@ @RequestMapping("/hello_world") public class HelloWorldResource { - // We have a greeting we read from the properties file using the Spring Expression Language - @Value("${stenden.greeting}") - private String greeting; + // We have a greeting we read from the properties file using the Spring Expression Language + @Value("${stenden.greeting}") + private String greeting; - /** - * Finally, your first endpoint! And this is a GET endpoint, as you can see in the annotation - * - * @return a message with a greeting - */ - @GetMapping - public Message helloWorld() { - return new Message(greeting); - } + /** + * Finally, your first endpoint! And this is a GET endpoint, as you can see in the annotation + * + * @return a message with a greeting + */ + @GetMapping + public Message helloWorld() { + return new Message(greeting); + } - /** - * Now let's grab a parameter from the URL - * - * @param name - * @return - */ - @GetMapping("/custom") - public Message customGreeting(@RequestParam("name") String name) { - return new Message("Hello " + name + "!"); - } + /** + * Now let's grab a parameter from the URL + * + * @param name + * @return + */ + @GetMapping("/custom") + public Message customGreeting(@RequestParam("name") String name) { + return new Message("Hello " + name + "!"); + } - /** - * Let's demonstrate an exception - * - * @return This method will always fail! - */ - @GetMapping("/error") - public Message failedHelloWorld() { - throw new GreetingException("I can't be bothered"); - } + /** + * Let's demonstrate an exception + * + * @return This method will always fail! + */ + @GetMapping("/error") + public Message failedHelloWorld() { + throw new GreetingException("I can't be bothered"); + } } diff --git a/src/main/java/stenden/spring/resource/HouseResource.java b/src/main/java/stenden/spring/resource/HouseResource.java index 82898c4..ab7bef5 100644 --- a/src/main/java/stenden/spring/resource/HouseResource.java +++ b/src/main/java/stenden/spring/resource/HouseResource.java @@ -14,21 +14,21 @@ @RequestMapping("/data") public class HouseResource { - private final HouseService houseService; + private final HouseService houseService; - @Autowired - public HouseResource(HouseService houseService) { - this.houseService = houseService; - } + @Autowired + public HouseResource(HouseService houseService) { + this.houseService = houseService; + } - @GetMapping("/jpa/{id}") - public House getJpaHouse(@PathVariable("id") Long id) { - return houseService.getJpaHouse(id); - } + @GetMapping("/jpa/{id}") + public House getJpaHouse(@PathVariable("id") Long id) { + return houseService.getJpaHouse(id); + } - @PostMapping - public House postJpaHouse(@RequestBody AnnotatedHouse house) { - return houseService.addHouse(house); - } + @PostMapping + public House postJpaHouse(@RequestBody AnnotatedHouse house) { + return houseService.addHouse(house); + } } diff --git a/src/main/java/stenden/spring/resource/Message.java b/src/main/java/stenden/spring/resource/Message.java index 9cad62c..fad1a93 100644 --- a/src/main/java/stenden/spring/resource/Message.java +++ b/src/main/java/stenden/spring/resource/Message.java @@ -7,6 +7,6 @@ @AllArgsConstructor public class Message { - private String message; + private String message; } diff --git a/src/main/java/stenden/spring/security/JWTFilter.java b/src/main/java/stenden/spring/security/JWTFilter.java index 515b59c..235de87 100644 --- a/src/main/java/stenden/spring/security/JWTFilter.java +++ b/src/main/java/stenden/spring/security/JWTFilter.java @@ -11,14 +11,27 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; +/** + * We use this filter to read and verify the JWT in the incoming HTTP Request. + */ public class JWTFilter extends GenericFilterBean { + // We use the JWTProvider to verify tokens on incoming requests private JWTProvider jwtProvider; public JWTFilter(JWTProvider jwtProvider) { this.jwtProvider = jwtProvider; } + /** + * Execute the filter. + * + * @param request The HTTP Request that triggered this whole chain + * @param response The response we'll be returning to the client + * @param chain The filter chain. + * @throws IOException + * @throws ServletException + */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { diff --git a/src/main/java/stenden/spring/security/JWTProvider.java b/src/main/java/stenden/spring/security/JWTProvider.java index 6c8272a..7b7aae9 100644 --- a/src/main/java/stenden/spring/security/JWTProvider.java +++ b/src/main/java/stenden/spring/security/JWTProvider.java @@ -13,47 +13,81 @@ import java.util.Base64; import java.util.Date; +/** + * Class for generating, validating and refreshing validation JSON Web Tokens. + */ public class JWTProvider { + // This is the algorithm we use for signing the JSON Web Token private static final SignatureAlgorithm ALGORITHM = SignatureAlgorithm.HS256; + // We use this service to retrieve users private final UserDetailsService myUserDetails; + // When we sign a JSON Web Token, we use a secret key so nobody can re-sign an edited token and present it as valid private String secretKey; - private long validityInMilliseconds = 600000; // 1minute + // For how long do we want a token to stay valid? + private long validityInMilliseconds = 600000; // 10 minutes + public JWTProvider(UserDetailsService myUserDetails, String secretKey) { this.myUserDetails = myUserDetails; this.secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); } + /** + * We're creating a token for a given username. + * + * @param username The username to create a token for + * @return A JSON Web Token, signed with a secret key. + */ public String createToken(String username) { Date now = new Date(); Date expiration = new Date(now.getTime() + validityInMilliseconds); - return Jwts.builder()// - .setSubject(username)// - .setIssuedAt(now)// - .setExpiration(expiration)// - .signWith(ALGORITHM, secretKey)// + return Jwts.builder() + .setSubject(username) + .setIssuedAt(now) + .setExpiration(expiration) + .signWith(ALGORITHM, secretKey) .compact(); } + /** + * Based on the token, we can retrieve the user from the database. + * First we parse the token and extract the username. We then use this to query the {@link UserDetailsService}. + * + * @param tokenString The token corresponding to a user + * @return The {@link Authentication} object corresponding to the requested user. + */ public Authentication getAuthentication(String tokenString) { - Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(tokenString); - String user = claims.getBody().getSubject(); + Claims claims = getClaims(tokenString); + String user = claims.getSubject(); UserDetails userDetails = myUserDetails.loadUserByUsername(user); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } + /** + * Check whether the token is about to expire, if yes, return a new one. + * + * @param tokenString The token + * @return null if the token is not close to expiring, a new one if it is. + */ public String getRefreshToken(String tokenString) { - Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(tokenString); - String user = claims.getBody().getSubject(); - Date expiration = claims.getBody().getExpiration(); + Claims claims = getClaims(tokenString); + String user = claims.getSubject(); + Date expiration = claims.getExpiration(); if (new Date(new Date().getTime() + validityInMilliseconds / 10).after(expiration)) { return createToken(user); } return null; } + /** + * Extract the token from the HTTP request. + * This is done by looking at the 'Authorization' request header. + * + * @param req The HTTP Request + * @return The header, if one is found. + */ public String getToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { @@ -62,8 +96,27 @@ public String getToken(HttpServletRequest req) { return null; } - public boolean validateToken(String token) { - Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + /** + * Validate the token. This is done by simply parsing it and then checking whether the used algorithm + * is the one we used. + * The parsing will fail and throw an exception if the token has expired. + * + * @param tokenString The token + * @return True if the token is valid, false if not. + */ + public boolean validateToken(String tokenString) { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(tokenString); return SignatureAlgorithm.forName(claims.getHeader().getAlgorithm()) == ALGORITHM; } + + /** + * Parse the JSON Web Token and return the claims from it. + * + * @param tokenString The token + * @return The claims + */ + private Claims getClaims(String tokenString) { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(tokenString); + return claims.getBody(); + } } diff --git a/src/main/java/stenden/spring/security/JWTStatusHandler.java b/src/main/java/stenden/spring/security/JWTStatusHandler.java index ae7a5ba..8d2a3bb 100644 --- a/src/main/java/stenden/spring/security/JWTStatusHandler.java +++ b/src/main/java/stenden/spring/security/JWTStatusHandler.java @@ -7,14 +7,25 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +/** + * This is used when the authentication of an user succeeds. + */ public class JWTStatusHandler implements AuthenticationSuccessHandler { + // The JWTProvider for creating JSON Web Tokens private final JWTProvider jwtProvider; public JWTStatusHandler(JWTProvider jwtProvider) { this.jwtProvider = jwtProvider; } + /** + * On a successful authentication attempt, we'll return a 200 and a JSON Web Token. + * + * @param request The request that triggered the authentication + * @param response The response that is going back to the client. We can write to this object. + * @param authentication The Authentication object, representing the successful authentication + */ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { diff --git a/src/main/java/stenden/spring/service/HouseService.java b/src/main/java/stenden/spring/service/HouseService.java index e248b89..9635eac 100644 --- a/src/main/java/stenden/spring/service/HouseService.java +++ b/src/main/java/stenden/spring/service/HouseService.java @@ -9,19 +9,19 @@ @Service public class HouseService { - private final HouseRepository entityManagerJpaRepository; + private final HouseRepository entityManagerJpaRepository; - @Autowired - public HouseService(HouseRepository entityManagerJpaRepository) { - this.entityManagerJpaRepository = entityManagerJpaRepository; - } + @Autowired + public HouseService(HouseRepository entityManagerJpaRepository) { + this.entityManagerJpaRepository = entityManagerJpaRepository; + } - public House getJpaHouse(Long id) { - return entityManagerJpaRepository.getByID(id); - } + public House getJpaHouse(Long id) { + return entityManagerJpaRepository.getByID(id); + } - public House addHouse(AnnotatedHouse house) { - return entityManagerJpaRepository.addHouse(house); - } + public House addHouse(AnnotatedHouse house) { + return entityManagerJpaRepository.addHouse(house); + } } From bf3ec83f1aeb77d886d6d2711eebec2215043b53 Mon Sep 17 00:00:00 2001 From: Sander ten Hoor Date: Wed, 11 Dec 2019 10:14:02 +0100 Subject: [PATCH 12/18] ADD: Integration test --- pom.xml | 50 ++++++++++++ .../spring/security/HTTPStatusHandler.java | 4 +- .../stenden/spring/security/JWTFilter.java | 4 +- .../stenden/spring/service/HouseService.java | 1 + .../it/HouseControllerIntegrationTest.java | 78 +++++++++++++++++++ 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java diff --git a/pom.xml b/pom.xml index 58bf73a..1429c9f 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,27 @@ ${spring.version} + + org.springframework + spring-test + ${spring.version} + + + + + org.hamcrest + hamcrest + 2.2 + test + + + + + org.springframework.security + spring-security-test + ${spring.version} + + javax.servlet @@ -119,6 +140,35 @@ 2.5.2 + + org.junit.jupiter + junit-jupiter + 5.5.2 + test + + + + org.assertj + assertj-core + 3.14.0 + test + + + + org.mockito + mockito-junit-jupiter + 3.1.0 + test + + + + org.apache.httpcomponents + httpclient + 4.5.10 + test + + + diff --git a/src/main/java/stenden/spring/security/HTTPStatusHandler.java b/src/main/java/stenden/spring/security/HTTPStatusHandler.java index f4deb87..c3b581a 100644 --- a/src/main/java/stenden/spring/security/HTTPStatusHandler.java +++ b/src/main/java/stenden/spring/security/HTTPStatusHandler.java @@ -6,6 +6,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; public class HTTPStatusHandler implements AuthenticationFailureHandler { @@ -17,8 +18,9 @@ public HTTPStatusHandler(HttpStatus status) { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) { + AuthenticationException exception) throws IOException { response.setStatus(status.value()); + response.getWriter().write("{\"message\":\"Invalid credentials.\"}"); } } diff --git a/src/main/java/stenden/spring/security/JWTFilter.java b/src/main/java/stenden/spring/security/JWTFilter.java index 235de87..f5c20f0 100644 --- a/src/main/java/stenden/spring/security/JWTFilter.java +++ b/src/main/java/stenden/spring/security/JWTFilter.java @@ -1,5 +1,6 @@ package stenden.spring.security; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.GenericFilterBean; @@ -41,7 +42,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha // If there was a token and it is valid (e.g.: not expired) if (token != null && jwtProvider.validateToken(token)) { // Get the authentication instance and set it in the SecurityContext - SecurityContextHolder.getContext().setAuthentication(jwtProvider.getAuthentication(token)); + Authentication authentication = jwtProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); // Check if the token is about to expire and we need to generate a fresh one String newToken = jwtProvider.getRefreshToken(token); diff --git a/src/main/java/stenden/spring/service/HouseService.java b/src/main/java/stenden/spring/service/HouseService.java index 9635eac..806abd1 100644 --- a/src/main/java/stenden/spring/service/HouseService.java +++ b/src/main/java/stenden/spring/service/HouseService.java @@ -7,6 +7,7 @@ import stenden.spring.data.model.AnnotatedHouse; @Service + public class HouseService { private final HouseRepository entityManagerJpaRepository; diff --git a/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java new file mode 100644 index 0000000..25c0f48 --- /dev/null +++ b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java @@ -0,0 +1,78 @@ +package stenden.spring.it; + +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import stenden.spring.configuration.WebConfig; + +import java.util.Arrays; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = WebConfig.class) +@WebAppConfiguration +public class HouseControllerIntegrationTest { + + @Autowired + private WebApplicationContext wac; + + private MockMvc mockMvc; + + @BeforeEach + public void setup() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(wac) + .apply(springSecurity()) + .build(); + } + + @Test + public void testAuthentication() throws Exception { + authenticate("user", "password") + .andExpect(header().exists("jwt-token")); + } + + @Test + public void testRetrieveData() throws Exception { + String jwt = validJWT(); + mockMvc.perform( + get("/data/jpa/1") + .header("Authorization", "Bearer " + jwt)) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect( + content().string("{\"id\":1,\"nrOfFloors\":4,\"nrOfRooms\":12,\"street\":\"Ubbo Emmiussingel 112\",\"city\":\"Groningen\"}") + ); + } + + private ResultActions authenticate(String user, String password) throws Exception { + return mockMvc.perform( + post("/login") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .content(EntityUtils.toString(new UrlEncodedFormEntity(Arrays.asList( + new BasicNameValuePair("username", user), + new BasicNameValuePair("password", password) + )))) + ); + } + + private String validJWT() throws Exception { + return authenticate("user", "password").andReturn().getResponse().getHeader("jwt-token"); + } + +} From 5ba6a97b6848d9878a7405ea8f17ec8d2d901242 Mon Sep 17 00:00:00 2001 From: Sander Date: Mon, 20 Sep 2021 11:16:54 +0200 Subject: [PATCH 13/18] ADD: Exception handling test --- .../spring/configuration/DatabaseConfig.java | 30 +++++++++++-------- .../spring/configuration/SecurityConfig.java | 26 ++++++++-------- .../it/HouseControllerIntegrationTest.java | 10 ++++++- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index eed8bdc..b0c4ab1 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -6,6 +6,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -22,19 +24,21 @@ @EnableTransactionManagement // Required for Hibernate public class DatabaseConfig { - /** - * The {@link DataSource} representing the database connection. - * - * @return The connection to the database - */ - @Bean - public DataSource dataSource() throws SQLException { - MariaDbPoolDataSource dataSource = new MariaDbPoolDataSource(); - dataSource.setUser("root"); - dataSource.setPassword("root"); - dataSource.setUrl("jdbc:mariadb://localhost:3306/example"); - return dataSource; - } + /** + * The {@link DataSource} representing the database connection. + * In our case we're creating an in-memory database using H2, + * so the setup is simple. + * + * @return The connection to the database + */ + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.H2) + .addScript("classpath:schema.sql") + .addScript("classpath:insert-data.sql") + .build(); + } /** * Whhn just using JPA, you could also use this transaction manager. diff --git a/src/main/java/stenden/spring/configuration/SecurityConfig.java b/src/main/java/stenden/spring/configuration/SecurityConfig.java index fa0b01b..032e754 100644 --- a/src/main/java/stenden/spring/configuration/SecurityConfig.java +++ b/src/main/java/stenden/spring/configuration/SecurityConfig.java @@ -151,23 +151,23 @@ private HttpSecurity enableRESTAuthentication(HttpSecurity http) throws Exceptio http // We'll allow logging in as if it were a form, using form headers .formLogin() - // Everyone is allowed to reach this endpoint - .permitAll() - // If login fails, return a 401 instead of redirecting, as is normal for form login - .failureHandler(new HTTPStatusHandler(HttpStatus.UNAUTHORIZED)) - // If login succeeds return a 200 instead of redirecting, as is normal for form login - // We also return a JSON Web Token - .successHandler(new JWTStatusHandler(jwtProvider)) + // Everyone is allowed to reach this endpoint + .permitAll() + // If login fails, return a 401 instead of redirecting, as is normal for form login + .failureHandler(new HTTPStatusHandler(HttpStatus.UNAUTHORIZED)) + // If login succeeds return a 200 instead of redirecting, as is normal for form login + // We also return a JSON Web Token + .successHandler(new JWTStatusHandler(jwtProvider)) // Configuring the HttpSecurity object is done in steps, we just configured the formLogin, // now we're continuing to exception handling. To signal this, we need to use and(), // otherwise we'd still be trying to configure formLogin. .and() - // What to do when it goes wrong? - .exceptionHandling() - // When the access is denied to a certain part of the application, use the given handler. - .accessDeniedHandler(restAccessDeniedHandler) - // When the authentication fails, use the given handler. - .authenticationEntryPoint(restAuthenticationEntryPoint); + // What to do when it goes wrong? + .exceptionHandling() + // When the access is denied to a certain part of the application, use the given handler. + .accessDeniedHandler(restAccessDeniedHandler) + // When the given JWT is invalid, use this handler. + .authenticationEntryPoint(restAuthenticationEntryPoint); // We need to register our JWTFilter. We register it before the UsernamePasswordAuthenticationFilter, // as that is part of the group of filters where Spring expects updates to the SecurityContextHolder diff --git a/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java index 25c0f48..d8a7bd3 100644 --- a/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java +++ b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java @@ -38,10 +38,18 @@ public class HouseControllerIntegrationTest { @BeforeEach public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(wac) - .apply(springSecurity()) + .apply(springSecurity()) // Required to enable Spring Security during the testing .build(); } + @Test + public void testException() throws Exception { + mockMvc.perform( + get("/hello_world/error") + .header("Authorization", "Bearer " + validJWT()) + ).andDo((x -> System.out.println(x.getResponse().getContentAsString()))); + } + @Test public void testAuthentication() throws Exception { authenticate("user", "password") From fccb7088b0ab107a187f660378070e5b43a3d264 Mon Sep 17 00:00:00 2001 From: Sander Date: Tue, 21 Sep 2021 12:03:06 +0200 Subject: [PATCH 14/18] ADD: IT tests --- pom.xml | 21 +++++++++++++++- .../spring/data/model/AnnotatedHouse.java | 4 ++++ .../spring/resource/ExceptionHandlers.java | 12 +++++++++- .../spring/resource/HouseResource.java | 3 ++- .../it/HouseControllerIntegrationTest.java | 24 +++++++++++++++---- .../java/stenden/spring/it/TestConfig.java | 16 +++++++++++++ 6 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 src/test/java/stenden/spring/it/TestConfig.java diff --git a/pom.xml b/pom.xml index 1429c9f..4172d10 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,25 @@ ${spring.version} + + + org.hibernate + hibernate-validator + 5.2.4.Final + + + + org.glassfish + javax.el + 3.0.0 + + + + javax.validation + validation-api + 2.0.1.Final + + org.springframework.security spring-security-config @@ -231,4 +250,4 @@ - \ No newline at end of file + diff --git a/src/main/java/stenden/spring/data/model/AnnotatedHouse.java b/src/main/java/stenden/spring/data/model/AnnotatedHouse.java index 8885fef..a3a608f 100644 --- a/src/main/java/stenden/spring/data/model/AnnotatedHouse.java +++ b/src/main/java/stenden/spring/data/model/AnnotatedHouse.java @@ -1,9 +1,12 @@ package stenden.spring.data.model; +import javax.validation.constraints.Max; +import javax.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; import stenden.spring.data.House; import javax.persistence.*; @@ -26,6 +29,7 @@ public class AnnotatedHouse implements House { private Integer nrOfRooms; @Column(name = "STREET") + @Size(max = 10) // This is just to test validation private String street; @Column(name = "CITY") diff --git a/src/main/java/stenden/spring/resource/ExceptionHandlers.java b/src/main/java/stenden/spring/resource/ExceptionHandlers.java index 364a423..0c9b64c 100644 --- a/src/main/java/stenden/spring/resource/ExceptionHandlers.java +++ b/src/main/java/stenden/spring/resource/ExceptionHandlers.java @@ -1,9 +1,14 @@ package stenden.spring.resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import javax.servlet.http.HttpServletRequest; @@ -11,7 +16,7 @@ * A class for handling exceptions */ @RestControllerAdvice -public class ExceptionHandlers { +public class ExceptionHandlers extends ResponseEntityExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(GreetingException.class) @@ -27,4 +32,9 @@ public ErrorResponse handleGreetingException( return new ErrorResponse(response); } + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { + return ResponseEntity.badRequest().body(new ErrorResponse("Try proper input next time")); + + } } diff --git a/src/main/java/stenden/spring/resource/HouseResource.java b/src/main/java/stenden/spring/resource/HouseResource.java index ab7bef5..07bd045 100644 --- a/src/main/java/stenden/spring/resource/HouseResource.java +++ b/src/main/java/stenden/spring/resource/HouseResource.java @@ -1,5 +1,6 @@ package stenden.spring.resource; +import javax.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import stenden.spring.data.House; @@ -27,7 +28,7 @@ public House getJpaHouse(@PathVariable("id") Long id) { } @PostMapping - public House postJpaHouse(@RequestBody AnnotatedHouse house) { + public House postJpaHouse(@RequestBody @Valid AnnotatedHouse house) { return houseService.addHouse(house); } diff --git a/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java index d8a7bd3..1107bc5 100644 --- a/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java +++ b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java @@ -22,11 +22,12 @@ import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +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.header; @ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = WebConfig.class) +@ContextConfiguration(classes = {WebConfig.class, TestConfig.class}) @WebAppConfiguration public class HouseControllerIntegrationTest { @@ -47,7 +48,22 @@ public void testException() throws Exception { mockMvc.perform( get("/hello_world/error") .header("Authorization", "Bearer " + validJWT()) - ).andDo((x -> System.out.println(x.getResponse().getContentAsString()))); + ).andDo(print()); // This is not a valid test, just an example of doing a request in an Integration Test + } + + @Test + public void testValidation() throws Exception { + mockMvc.perform( + post("/data") + .header("Authorization", "Bearer " + validJWT()) + .content("{\n" + + " \"nrOfFloors\": 2,\n" + + " \"nrOfRooms\": 2,\n" + + " \"street\": \"yesbutno\",\n" + + " \"city\": \"no\"\n" + + "}\n") + .contentType(MediaType.APPLICATION_JSON) + ).andDo(print()); // This is not a valid test, just an example of doing a request in an Integration Test } @Test @@ -60,8 +76,8 @@ public void testAuthentication() throws Exception { public void testRetrieveData() throws Exception { String jwt = validJWT(); mockMvc.perform( - get("/data/jpa/1") - .header("Authorization", "Bearer " + jwt)) + get("/data/jpa/1") + .header("Authorization", "Bearer " + jwt)) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect( content().string("{\"id\":1,\"nrOfFloors\":4,\"nrOfRooms\":12,\"street\":\"Ubbo Emmiussingel 112\",\"city\":\"Groningen\"}") diff --git a/src/test/java/stenden/spring/it/TestConfig.java b/src/test/java/stenden/spring/it/TestConfig.java new file mode 100644 index 0000000..1e513e9 --- /dev/null +++ b/src/test/java/stenden/spring/it/TestConfig.java @@ -0,0 +1,16 @@ +package stenden.spring.it; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +@Configuration +public class TestConfig { + + // Otherwise validation will fail during testing + @Bean + public MethodValidationPostProcessor getMethodValidationPostProcessor(){ + return new MethodValidationPostProcessor(); + } + +} From 36c01415660a741f67a52b9ee2d2d1d4ffef04a0 Mon Sep 17 00:00:00 2001 From: Sander Date: Mon, 6 Dec 2021 14:43:36 +0100 Subject: [PATCH 15/18] REF: Made the login into a RESTful endpoint --- .../spring/configuration/DatabaseConfig.java | 11 ++-- .../spring/configuration/SecurityConfig.java | 63 ++++++++++--------- .../spring/resource/ExceptionHandlers.java | 7 +++ .../spring/resource/LoginController.java | 45 +++++++++++++ .../spring/security/HTTPStatusHandler.java | 24 ------- .../spring/security/JWTStatusHandler.java | 37 ----------- .../stenden/spring/security/LoginDTO.java | 12 ++++ ...Point.java => UnauthenticatedHandler.java} | 3 +- ...dler.java => UserAccessDeniedHandler.java} | 8 +-- 9 files changed, 106 insertions(+), 104 deletions(-) create mode 100644 src/main/java/stenden/spring/resource/LoginController.java delete mode 100644 src/main/java/stenden/spring/security/HTTPStatusHandler.java delete mode 100644 src/main/java/stenden/spring/security/JWTStatusHandler.java create mode 100644 src/main/java/stenden/spring/security/LoginDTO.java rename src/main/java/stenden/spring/security/{RestAuthenticationEntryPoint.java => UnauthenticatedHandler.java} (89%) mode change 100755 => 100644 rename src/main/java/stenden/spring/security/{RestAccessDeniedHandler.java => UserAccessDeniedHandler.java} (75%) mode change 100755 => 100644 diff --git a/src/main/java/stenden/spring/configuration/DatabaseConfig.java b/src/main/java/stenden/spring/configuration/DatabaseConfig.java index eed8bdc..e9c2071 100644 --- a/src/main/java/stenden/spring/configuration/DatabaseConfig.java +++ b/src/main/java/stenden/spring/configuration/DatabaseConfig.java @@ -6,6 +6,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaVendorAdapter; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -29,11 +31,10 @@ public class DatabaseConfig { */ @Bean public DataSource dataSource() throws SQLException { - MariaDbPoolDataSource dataSource = new MariaDbPoolDataSource(); - dataSource.setUser("root"); - dataSource.setPassword("root"); - dataSource.setUrl("jdbc:mariadb://localhost:3306/example"); - return dataSource; + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2) + .addScript("classpath:schema.sql") + .addScript("classpath:insert-data.sql") + .build(); } /** diff --git a/src/main/java/stenden/spring/configuration/SecurityConfig.java b/src/main/java/stenden/spring/configuration/SecurityConfig.java index fa0b01b..7f973b5 100644 --- a/src/main/java/stenden/spring/configuration/SecurityConfig.java +++ b/src/main/java/stenden/spring/configuration/SecurityConfig.java @@ -4,7 +4,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; @@ -38,10 +38,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired - private RestAuthenticationEntryPoint restAuthenticationEntryPoint; - @Autowired - private RestAccessDeniedHandler restAccessDeniedHandler; - @Autowired private JWTProvider jwtProvider; /** @@ -84,7 +80,7 @@ public JWTProvider jwtProvider(UserDetailsService userDetailsService, @Value("se /** * We're configuring the {@link AuthenticationManagerBuilder} to tell it where to get the users from. - * By default Spring expects certain tables to be present if you simpy use {@link AuthenticationManagerBuilder#jdbcAuthentication()} + * By default, Spring expects certain tables to be present if you simpy use {@link AuthenticationManagerBuilder#jdbcAuthentication()} * but you can customize that by giving your own queries, as shown in the method. * You still need to make sure the query returns the right columns. * You can also implement your own {@link UserDetailsService} where you'll retrieve users instead. @@ -107,6 +103,17 @@ public void configure(AuthenticationManagerBuilder auth) throws Exception { .setAuthoritiesByUsernameQuery("SELECT USERNAME, ROLE FROM USERS WHERE USERNAME = ?"); } + /** + * We expose the AuthenticationManager to our application so we can use it to authenticate login requests + * @return + * @throws Exception + */ + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + /** * In this method we get a {@link HttpSecurity} object we can use for configuring the security in our application. *

@@ -124,19 +131,19 @@ protected void configure(HttpSecurity http) throws Exception { enableRESTAuthentication(http) // Now let's say which requests we want to authorize .authorizeRequests() - // Every single request needs to be authorized... with the exception of /login, - // which was defined in enableRESTAuthentication(), which was earlier and thus trumps this configuration. - .anyRequest() - // We require every user to have the role USER (this translates to 'ROLE_USER' in the database - // and in other places) - .hasRole("USER") + // Every single request needs to be authorized... with the exception of /authorization, + // which was defined in enableRESTAuthentication(), which was earlier and thus trumps this configuration. + .anyRequest() + // We require every user to have the role USER (this translates to 'ROLE_USER' in the database + // and in other places) + .hasRole("USER") // Alright, we're done, let's move on to the next part using and() .and() - // We're disabling defenses against Cross-Site Request Forgery, - // as the browser is not responsible for adding authentication information to the request - // which is wat the CSRF exploit relies on. - .csrf() - .disable(); + // We're disabling defenses against Cross-Site Request Forgery, + // as the browser is not responsible for adding authentication information to the request + // which is wat the CSRF exploit relies on. + .csrf() + .disable(); } /** @@ -149,25 +156,19 @@ protected void configure(HttpSecurity http) throws Exception { */ private HttpSecurity enableRESTAuthentication(HttpSecurity http) throws Exception { http - // We'll allow logging in as if it were a form, using form headers - .formLogin() - // Everyone is allowed to reach this endpoint - .permitAll() - // If login fails, return a 401 instead of redirecting, as is normal for form login - .failureHandler(new HTTPStatusHandler(HttpStatus.UNAUTHORIZED)) - // If login succeeds return a 200 instead of redirecting, as is normal for form login - // We also return a JSON Web Token - .successHandler(new JWTStatusHandler(jwtProvider)) + .authorizeRequests() + .mvcMatchers("/authenticate") + .permitAll() // Configuring the HttpSecurity object is done in steps, we just configured the formLogin, // now we're continuing to exception handling. To signal this, we need to use and(), // otherwise we'd still be trying to configure formLogin. .and() // What to do when it goes wrong? - .exceptionHandling() - // When the access is denied to a certain part of the application, use the given handler. - .accessDeniedHandler(restAccessDeniedHandler) - // When the authentication fails, use the given handler. - .authenticationEntryPoint(restAuthenticationEntryPoint); + .exceptionHandling() + // When the access is denied to a certain part of the application, use the given handler. + .accessDeniedHandler(new UserAccessDeniedHandler()) + // When a user tries to reach an endpoint without a JWT, use the following handler. + .authenticationEntryPoint(new UnauthenticatedHandler()); // We need to register our JWTFilter. We register it before the UsernamePasswordAuthenticationFilter, // as that is part of the group of filters where Spring expects updates to the SecurityContextHolder diff --git a/src/main/java/stenden/spring/resource/ExceptionHandlers.java b/src/main/java/stenden/spring/resource/ExceptionHandlers.java index 364a423..2f71ee4 100644 --- a/src/main/java/stenden/spring/resource/ExceptionHandlers.java +++ b/src/main/java/stenden/spring/resource/ExceptionHandlers.java @@ -1,6 +1,7 @@ package stenden.spring.resource; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -27,4 +28,10 @@ public ErrorResponse handleGreetingException( return new ErrorResponse(response); } + @ResponseStatus(HttpStatus.UNAUTHORIZED) + @ExceptionHandler(BadCredentialsException.class) + public ErrorResponse handleBadCredentialsException() { + return new ErrorResponse("Invalid credentials"); + } + } diff --git a/src/main/java/stenden/spring/resource/LoginController.java b/src/main/java/stenden/spring/resource/LoginController.java new file mode 100644 index 0000000..4b84972 --- /dev/null +++ b/src/main/java/stenden/spring/resource/LoginController.java @@ -0,0 +1,45 @@ +package stenden.spring.resource; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import stenden.spring.security.JWTProvider; +import stenden.spring.security.LoginDTO; + +@RestController +@RequestMapping("/authenticate") +public class LoginController { + + private final AuthenticationManager authenticationManager; + + private final JWTProvider jwtProvider; + + public LoginController(AuthenticationManager authenticationManager, JWTProvider jwtProvider) { + this.authenticationManager = authenticationManager; + this.jwtProvider = jwtProvider; + } + + + @PostMapping + public ResponseEntity login(@RequestBody LoginDTO login) { + System.out.println(login); + Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword())); + + String username = ((UserDetails) authentication.getPrincipal()).getUsername(); + + String token = jwtProvider.createToken(username); + + return ResponseEntity.ok() + .header("jwt-token", token) + .build(); + } + + + +} diff --git a/src/main/java/stenden/spring/security/HTTPStatusHandler.java b/src/main/java/stenden/spring/security/HTTPStatusHandler.java deleted file mode 100644 index f4deb87..0000000 --- a/src/main/java/stenden/spring/security/HTTPStatusHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -package stenden.spring.security; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class HTTPStatusHandler implements AuthenticationFailureHandler { - - private HttpStatus status; - - public HTTPStatusHandler(HttpStatus status) { - this.status = status; - } - - @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, - AuthenticationException exception) { - response.setStatus(status.value()); - } - -} diff --git a/src/main/java/stenden/spring/security/JWTStatusHandler.java b/src/main/java/stenden/spring/security/JWTStatusHandler.java deleted file mode 100644 index 8d2a3bb..0000000 --- a/src/main/java/stenden/spring/security/JWTStatusHandler.java +++ /dev/null @@ -1,37 +0,0 @@ -package stenden.spring.security; - -import org.springframework.http.HttpStatus; -import org.springframework.security.core.Authentication; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * This is used when the authentication of an user succeeds. - */ -public class JWTStatusHandler implements AuthenticationSuccessHandler { - - // The JWTProvider for creating JSON Web Tokens - private final JWTProvider jwtProvider; - - public JWTStatusHandler(JWTProvider jwtProvider) { - this.jwtProvider = jwtProvider; - } - - /** - * On a successful authentication attempt, we'll return a 200 and a JSON Web Token. - * - * @param request The request that triggered the authentication - * @param response The response that is going back to the client. We can write to this object. - * @param authentication The Authentication object, representing the successful authentication - */ - @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, - Authentication authentication) { - String token = jwtProvider.createToken(authentication.getName()); - response.addHeader("jwt-token", token); - response.setStatus(HttpStatus.OK.value()); - } - -} diff --git a/src/main/java/stenden/spring/security/LoginDTO.java b/src/main/java/stenden/spring/security/LoginDTO.java new file mode 100644 index 0000000..d78156b --- /dev/null +++ b/src/main/java/stenden/spring/security/LoginDTO.java @@ -0,0 +1,12 @@ +package stenden.spring.security; + +import lombok.Data; + +@Data +public class LoginDTO { + + private String username; + + private String password; + +} diff --git a/src/main/java/stenden/spring/security/RestAuthenticationEntryPoint.java b/src/main/java/stenden/spring/security/UnauthenticatedHandler.java old mode 100755 new mode 100644 similarity index 89% rename from src/main/java/stenden/spring/security/RestAuthenticationEntryPoint.java rename to src/main/java/stenden/spring/security/UnauthenticatedHandler.java index 5da2f68..f7288bf --- a/src/main/java/stenden/spring/security/RestAuthenticationEntryPoint.java +++ b/src/main/java/stenden/spring/security/UnauthenticatedHandler.java @@ -9,8 +9,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; -@Component -public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { +public class UnauthenticatedHandler implements AuthenticationEntryPoint { @Override public void commence( HttpServletRequest httpServletRequest, diff --git a/src/main/java/stenden/spring/security/RestAccessDeniedHandler.java b/src/main/java/stenden/spring/security/UserAccessDeniedHandler.java old mode 100755 new mode 100644 similarity index 75% rename from src/main/java/stenden/spring/security/RestAccessDeniedHandler.java rename to src/main/java/stenden/spring/security/UserAccessDeniedHandler.java index 2e634ee..575abee --- a/src/main/java/stenden/spring/security/RestAccessDeniedHandler.java +++ b/src/main/java/stenden/spring/security/UserAccessDeniedHandler.java @@ -9,15 +9,13 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; -@Component -public class RestAccessDeniedHandler implements AccessDeniedHandler { +public class UserAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle( HttpServletRequest request, HttpServletResponse response, - AccessDeniedException accessDeniedException) throws IOException, ServletException { + AccessDeniedException accessDeniedException) throws IOException { response.setStatus(403); - response.getWriter().write("{\"message\":\"You\'re not allowed in here.\"}"); - + response.getWriter().write("{\"message\":\"You're not allowed in here.\"}"); } } From 4e4ac43b82b27272812998606039b3d30ae97810 Mon Sep 17 00:00:00 2001 From: Sander Date: Mon, 6 Dec 2021 16:23:13 +0100 Subject: [PATCH 16/18] REF: Made the login into a RESTful endpoint --- .../spring/configuration/SecurityConfig.java | 16 +++++++++++----- .../stenden/spring/resource/LoginController.java | 1 - .../java/stenden/spring/security/JWTFilter.java | 11 +++++++---- .../stenden/spring/security/JWTProvider.java | 8 ++++---- .../spring/security/UnauthenticatedHandler.java | 7 +++++-- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/main/java/stenden/spring/configuration/SecurityConfig.java b/src/main/java/stenden/spring/configuration/SecurityConfig.java index 7f973b5..9962d25 100644 --- a/src/main/java/stenden/spring/configuration/SecurityConfig.java +++ b/src/main/java/stenden/spring/configuration/SecurityConfig.java @@ -53,6 +53,9 @@ public PasswordEncoder encoder() { /** * To expose an UserDetailService in our application, * we can just override this method from the superclass. + * It will then create an UserDetailsService based on the + * AuthenticationManagerBuilder which we're configuring in + * {@link #configure(AuthenticationManagerBuilder)}. *

* We can also create our own UserDetailService if we so desire, * to customize how users are retrieved! @@ -67,10 +70,10 @@ public UserDetailsService userDetailsServiceBean() throws Exception { } /** - * We create the JWTProvider bean, which + * We create the JWTProvider bean, which is used for generating and verifying JWT's. * - * @param userDetailsService - * @param secretKey + * @param userDetailsService For retrieving the user while creating the JWT. + * @param secretKey The phrase used to encode the JWT in such a way only we can produce it. * @return */ @Bean @@ -104,7 +107,7 @@ public void configure(AuthenticationManagerBuilder auth) throws Exception { } /** - * We expose the AuthenticationManager to our application so we can use it to authenticate login requests + * We expose the AuthenticationManager to our application, so we can use it to authenticate login requests * @return * @throws Exception */ @@ -115,7 +118,9 @@ public AuthenticationManager authenticationManagerBean() throws Exception { } /** - * In this method we get a {@link HttpSecurity} object we can use for configuring the security in our application. + * In this method we get a {@link HttpSecurity} object we can use for securing the access to our application. + * The previous ones were focused on the inner workings, but this one is for deciding what happens + * when a client sends us a message. *

* It's important to know that the top most configuration should be more specific than the bottom configuration. * If I say the url /admin is accessible by admins only and then say all the URLS are open for everyone, @@ -128,6 +133,7 @@ public AuthenticationManager authenticationManagerBean() throws Exception { @Override protected void configure(HttpSecurity http) throws Exception { // First we configure it to allow authentication and authorization in REST + // This is just a helper method made by me to split it up enableRESTAuthentication(http) // Now let's say which requests we want to authorize .authorizeRequests() diff --git a/src/main/java/stenden/spring/resource/LoginController.java b/src/main/java/stenden/spring/resource/LoginController.java index 4b84972..db12378 100644 --- a/src/main/java/stenden/spring/resource/LoginController.java +++ b/src/main/java/stenden/spring/resource/LoginController.java @@ -28,7 +28,6 @@ public LoginController(AuthenticationManager authenticationManager, JWTProvider @PostMapping public ResponseEntity login(@RequestBody LoginDTO login) { - System.out.println(login); Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword())); String username = ((UserDetails) authentication.getPrincipal()).getUsername(); diff --git a/src/main/java/stenden/spring/security/JWTFilter.java b/src/main/java/stenden/spring/security/JWTFilter.java index 235de87..cfb7c0f 100644 --- a/src/main/java/stenden/spring/security/JWTFilter.java +++ b/src/main/java/stenden/spring/security/JWTFilter.java @@ -17,14 +17,16 @@ public class JWTFilter extends GenericFilterBean { // We use the JWTProvider to verify tokens on incoming requests - private JWTProvider jwtProvider; + private final JWTProvider jwtProvider; public JWTFilter(JWTProvider jwtProvider) { this.jwtProvider = jwtProvider; } /** - * Execute the filter. + * Check whether the filter has a valid JSON Web Token. + * If so, we place an Authentication object in the {@link SecurityContextHolder}. + * If there is no valid JWT, the {@link SecurityContextHolder} stays empty. * * @param request The HTTP Request that triggered this whole chain * @param response The response we'll be returning to the client @@ -38,7 +40,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha // Get the token from the request String token = jwtProvider.getToken((HttpServletRequest) request); try { - // If there was a token and it is valid (e.g.: not expired) + // If there was a token, and it is valid (e.g.: not expired) if (token != null && jwtProvider.validateToken(token)) { // Get the authentication instance and set it in the SecurityContext SecurityContextHolder.getContext().setAuthentication(jwtProvider.getAuthentication(token)); @@ -55,7 +57,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha SecurityContextHolder.clearContext(); } - // Let the filter chain go on + // Let the filter chain go on, authentication failure should not stop the filter. + // It's up to Spring to decide whether the authentication in hte SecurityHolder is sufficient. chain.doFilter(request, response); } diff --git a/src/main/java/stenden/spring/security/JWTProvider.java b/src/main/java/stenden/spring/security/JWTProvider.java index 7b7aae9..73356ce 100644 --- a/src/main/java/stenden/spring/security/JWTProvider.java +++ b/src/main/java/stenden/spring/security/JWTProvider.java @@ -21,15 +21,15 @@ public class JWTProvider { // This is the algorithm we use for signing the JSON Web Token private static final SignatureAlgorithm ALGORITHM = SignatureAlgorithm.HS256; // We use this service to retrieve users - private final UserDetailsService myUserDetails; + private final UserDetailsService userDetailsService; // When we sign a JSON Web Token, we use a secret key so nobody can re-sign an edited token and present it as valid private String secretKey; // For how long do we want a token to stay valid? private long validityInMilliseconds = 600000; // 10 minutes - public JWTProvider(UserDetailsService myUserDetails, String secretKey) { - this.myUserDetails = myUserDetails; + public JWTProvider(UserDetailsService userDetailsService, String secretKey) { + this.userDetailsService = userDetailsService; this.secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); } @@ -60,7 +60,7 @@ public String createToken(String username) { public Authentication getAuthentication(String tokenString) { Claims claims = getClaims(tokenString); String user = claims.getSubject(); - UserDetails userDetails = myUserDetails.loadUserByUsername(user); + UserDetails userDetails = userDetailsService.loadUserByUsername(user); return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); } diff --git a/src/main/java/stenden/spring/security/UnauthenticatedHandler.java b/src/main/java/stenden/spring/security/UnauthenticatedHandler.java index f7288bf..a54793a 100644 --- a/src/main/java/stenden/spring/security/UnauthenticatedHandler.java +++ b/src/main/java/stenden/spring/security/UnauthenticatedHandler.java @@ -9,13 +9,16 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; +/** + * We use this class to send a response if + */ public class UnauthenticatedHandler implements AuthenticationEntryPoint { @Override public void commence( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, - AuthenticationException e) throws IOException, ServletException { + AuthenticationException e) throws IOException { httpServletResponse.setStatus(401); - httpServletResponse.getWriter().write("{\"message\":\"You're not authorized.\"}"); + httpServletResponse.getWriter().write("{\"message\":\"You're not authenticated.\"}"); } } From 0a1762e628fb822a552fe227ea59691a4b10a532 Mon Sep 17 00:00:00 2001 From: Sander Date: Mon, 6 Dec 2021 16:36:27 +0100 Subject: [PATCH 17/18] ENH: Updated the Security Integration test --- .../stenden/spring/security/LoginDTO.java | 4 ++ .../it/HouseControllerIntegrationTest.java | 16 +++---- .../it/HouseControllerIntegrationTest2.java | 44 +++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 src/test/java/stenden/spring/it/HouseControllerIntegrationTest2.java diff --git a/src/main/java/stenden/spring/security/LoginDTO.java b/src/main/java/stenden/spring/security/LoginDTO.java index d78156b..22dd332 100644 --- a/src/main/java/stenden/spring/security/LoginDTO.java +++ b/src/main/java/stenden/spring/security/LoginDTO.java @@ -1,8 +1,12 @@ package stenden.spring.security; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@AllArgsConstructor +@NoArgsConstructor public class LoginDTO { private String username; diff --git a/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java index 1107bc5..2d8fec5 100644 --- a/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java +++ b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest.java @@ -1,5 +1,6 @@ package stenden.spring.it; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; @@ -10,12 +11,14 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import stenden.spring.configuration.WebConfig; +import stenden.spring.security.LoginDTO; import java.util.Arrays; @@ -26,9 +29,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -@ExtendWith(SpringExtension.class) -@ContextConfiguration(classes = {WebConfig.class, TestConfig.class}) -@WebAppConfiguration +@SpringJUnitWebConfig(classes = {WebConfig.class, TestConfig.class}) public class HouseControllerIntegrationTest { @Autowired @@ -86,12 +87,9 @@ public void testRetrieveData() throws Exception { private ResultActions authenticate(String user, String password) throws Exception { return mockMvc.perform( - post("/login") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .content(EntityUtils.toString(new UrlEncodedFormEntity(Arrays.asList( - new BasicNameValuePair("username", user), - new BasicNameValuePair("password", password) - )))) + post("/authenticate") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(new LoginDTO(user, password))) ); } diff --git a/src/test/java/stenden/spring/it/HouseControllerIntegrationTest2.java b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest2.java new file mode 100644 index 0000000..39fe96c --- /dev/null +++ b/src/test/java/stenden/spring/it/HouseControllerIntegrationTest2.java @@ -0,0 +1,44 @@ +package stenden.spring.it; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import stenden.spring.configuration.WebConfig; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; + +@SpringJUnitWebConfig(classes = {WebConfig.class, TestConfig.class}) +public class HouseControllerIntegrationTest2 { + + @Autowired + private WebApplicationContext wac; + + private MockMvc mockMvc; + + @BeforeEach + public void setup() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(wac) + .apply(springSecurity()) // Required to enable Spring Security during the testing + .build(); + } + + @Test + @WithMockUser(username = "user") + public void testRetrieveData() throws Exception { + mockMvc.perform( + get("/data/jpa/1")) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect( + content().string("{\"id\":1,\"nrOfFloors\":4,\"nrOfRooms\":12,\"street\":\"Ubbo Emmiussingel 112\",\"city\":\"Groningen\"}") + ); + } + +} From d70115763233dcaf793be42561023756d115bb1b Mon Sep 17 00:00:00 2001 From: sthoor Date: Tue, 7 Dec 2021 15:18:16 +0100 Subject: [PATCH 18/18] FIX: Jackson version --- pom.xml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4172d10..a291bbe 100644 --- a/pom.xml +++ b/pom.xml @@ -101,7 +101,7 @@ com.fasterxml.jackson.core jackson-databind - 2.9.7 + 2.13.0 @@ -153,6 +153,20 @@ 1.1.1 + + + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.2 + + + + + org.glassfish.jaxb + jaxb-runtime + 2.3.2 + + org.mariadb.jdbc mariadb-java-client