From ba2f0bb095aa3cc367d0a8368a8c8a3ffd2d4962 Mon Sep 17 00:00:00 2001 From: Gustavo Martin Morcuende Date: Fri, 1 Jul 2016 02:37:29 +0200 Subject: [PATCH] JPA + Spring + REST --- SpringJava/JPA/pom.xml | 110 +++++++++++++-- .../LocalDateTimeAttributeConverter.java | 22 +++ .../de/spring/persistence/example/domain/Ad.java | 35 +++++ .../example/repository/AdRepository.java | 9 ++ .../de/spring/rest/controllers/AdController.java | 15 ++ .../common/exception/NotFoundException.java | 24 ++++ .../controller/RepositoryBasedRestController.java | 156 +++++++++++++++++++++ .../org/resthub/web/controller/RestController.java | 119 ++++++++++++++++ SpringJava/JPA/src/main/resources/jpa.properties | 4 +- SpringJava/JPA/src/main/resources/log4j2.xml | 37 +++-- .../spring-configuration/jpa-configuration.xml | 6 +- .../spring-configuration/mvc/rest/rest-config.xml | 93 ++++++++++++ SpringJava/JPA/src/main/webapp/WEB-INF/web.xml | 41 ++++++ .../spring/persistence/example/domain/AdTest.java | 5 + 14 files changed, 648 insertions(+), 28 deletions(-) create mode 100644 SpringJava/JPA/src/main/java/de/spring/persistence/converters/LocalDateTimeAttributeConverter.java create mode 100644 SpringJava/JPA/src/main/java/de/spring/persistence/example/domain/Ad.java create mode 100644 SpringJava/JPA/src/main/java/de/spring/persistence/example/repository/AdRepository.java create mode 100644 SpringJava/JPA/src/main/java/de/spring/rest/controllers/AdController.java create mode 100644 SpringJava/JPA/src/main/java/org/resthub/common/exception/NotFoundException.java create mode 100644 SpringJava/JPA/src/main/java/org/resthub/web/controller/RepositoryBasedRestController.java create mode 100644 SpringJava/JPA/src/main/java/org/resthub/web/controller/RestController.java create mode 100644 SpringJava/JPA/src/main/resources/spring-configuration/mvc/rest/rest-config.xml create mode 100644 SpringJava/JPA/src/main/webapp/WEB-INF/web.xml create mode 100644 SpringJava/JPA/src/test/java/de/spring/persistence/example/domain/AdTest.java diff --git a/SpringJava/JPA/pom.xml b/SpringJava/JPA/pom.xml index d91a5b4..c8a763a 100644 --- a/SpringJava/JPA/pom.xml +++ b/SpringJava/JPA/pom.xml @@ -3,23 +3,24 @@ 4.0.0 de.spring.jpa spring-jpa + war 1.0-SNAPSHOT spring-jpa - http://gumartinm.name + https://gumartinm.name/ JPA Spring Framework Gustavo Martin Morcuende - http://www.gumartinm.name + https://gumartinm.name/ - scm:git:http://git.gumartinm.name/SpringWebServicesForFun - http://git.gumartinm.name/SpringWebServicesForFun + scm:git:http://git.gumartinm.name/Spring/JPA + http://git.gumartinm.name/Spring/JPA UTF-8 UTF-8 - 4.2.4.RELEASE + 4.3.0.RELEASE @@ -42,7 +43,7 @@ org.apache.logging.log4j log4j-slf4j-impl - 2.5 + 2.6.1 + + javax.servlet + javax.servlet-api + 4.0.0-b01 + provided + + + com.fasterxml.jackson.core + jackson-databind + 2.6.4 + + + + + + javax.validation + validation-api + 1.1.0.Final + + + org.hibernate + hibernate-validator + 5.2.2.Final + @@ -152,6 +219,12 @@ spring-test ${spring.version} test + + + commons-logging + commons-logging + + org.mockito @@ -172,7 +245,6 @@ maven-failsafe-plugin 2.18.1 - org.apache.maven.plugins maven-compiler-plugin @@ -234,6 +306,22 @@ + + org.apache.maven.plugins + maven-war-plugin + 2.6 + + + + true + src/main/webapp + + WEB-INF/web.xml + + + + + diff --git a/SpringJava/JPA/src/main/java/de/spring/persistence/converters/LocalDateTimeAttributeConverter.java b/SpringJava/JPA/src/main/java/de/spring/persistence/converters/LocalDateTimeAttributeConverter.java new file mode 100644 index 0000000..2d2259c --- /dev/null +++ b/SpringJava/JPA/src/main/java/de/spring/persistence/converters/LocalDateTimeAttributeConverter.java @@ -0,0 +1,22 @@ +package de.spring.persistence.converters; + +import java.sql.Timestamp; +import java.time.LocalDateTime; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter(autoApply = true) +public class LocalDateTimeAttributeConverter implements AttributeConverter { + + @Override + public Timestamp convertToDatabaseColumn(LocalDateTime localDateTime) { + return (localDateTime == null ? null : Timestamp.valueOf(localDateTime)); + } + + @Override + public LocalDateTime convertToEntityAttribute(Timestamp sqlTimestamp) { + return (sqlTimestamp == null ? null : sqlTimestamp.toLocalDateTime()); + } + +} diff --git a/SpringJava/JPA/src/main/java/de/spring/persistence/example/domain/Ad.java b/SpringJava/JPA/src/main/java/de/spring/persistence/example/domain/Ad.java new file mode 100644 index 0000000..cc9fd31 --- /dev/null +++ b/SpringJava/JPA/src/main/java/de/spring/persistence/example/domain/Ad.java @@ -0,0 +1,35 @@ +package de.spring.persistence.example.domain; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(schema = "mybatis_example") +public class Ad implements Serializable { + + @Id + @GeneratedValue(strategy=GenerationType.IDENTITY) + @Column(name = "id", updatable = false, nullable = false) + private Long id; + + private Long companyId; + + @Column + private Long companyCategId; + + @Column + private String adMobileImage; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime updatedAt; +} diff --git a/SpringJava/JPA/src/main/java/de/spring/persistence/example/repository/AdRepository.java b/SpringJava/JPA/src/main/java/de/spring/persistence/example/repository/AdRepository.java new file mode 100644 index 0000000..8230c43 --- /dev/null +++ b/SpringJava/JPA/src/main/java/de/spring/persistence/example/repository/AdRepository.java @@ -0,0 +1,9 @@ +package de.spring.persistence.example.repository; + +import org.springframework.data.repository.PagingAndSortingRepository; + +import de.spring.persistence.example.domain.Ad; + +public interface AdRepository extends PagingAndSortingRepository { + +} diff --git a/SpringJava/JPA/src/main/java/de/spring/rest/controllers/AdController.java b/SpringJava/JPA/src/main/java/de/spring/rest/controllers/AdController.java new file mode 100644 index 0000000..16a61f9 --- /dev/null +++ b/SpringJava/JPA/src/main/java/de/spring/rest/controllers/AdController.java @@ -0,0 +1,15 @@ +package de.spring.rest.controllers; + +import org.resthub.web.controller.RepositoryBasedRestController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.spring.persistence.example.domain.Ad; +import de.spring.persistence.example.repository.AdRepository; + +@RestController +@RequestMapping("/ads/") +public class AdController extends RepositoryBasedRestController{ + + // I do not have to do anything here because all I need is implemented by RepositoryBasedRestController :) +} diff --git a/SpringJava/JPA/src/main/java/org/resthub/common/exception/NotFoundException.java b/SpringJava/JPA/src/main/java/org/resthub/common/exception/NotFoundException.java new file mode 100644 index 0000000..7135761 --- /dev/null +++ b/SpringJava/JPA/src/main/java/org/resthub/common/exception/NotFoundException.java @@ -0,0 +1,24 @@ +package org.resthub.common.exception; + +/** + * Exception thrown when not result was found (for example findById with null return value) + */ +public class NotFoundException extends RuntimeException { + + public NotFoundException() { + super(); + } + + public NotFoundException(final String message, final Throwable cause) { + super(message, cause); + } + + public NotFoundException(final String message) { + super(message); + } + + public NotFoundException(final Throwable cause) { + super(cause); + } + +} diff --git a/SpringJava/JPA/src/main/java/org/resthub/web/controller/RepositoryBasedRestController.java b/SpringJava/JPA/src/main/java/org/resthub/web/controller/RepositoryBasedRestController.java new file mode 100644 index 0000000..2fd4c10 --- /dev/null +++ b/SpringJava/JPA/src/main/java/org/resthub/web/controller/RepositoryBasedRestController.java @@ -0,0 +1,156 @@ +package org.resthub.web.controller; + +import org.resthub.common.exception.NotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.io.Serializable; +import java.util.Set; + +/** + * Abstract REST controller using a repository implementation + *

+ *

+ * You should extend this class when you want to use a 2 layers pattern : Repository and Controller. This is the default + * controller implementation to use if you have no service (also called business) layer. You will be able to transform + * it to a ServiceBasedRestController later easily if needed. + *

+ * + *

Default implementation uses "id" field (usually a Long) in order to identify resources in web request. + * If your want to identity resources by a slug (human readable identifier), your should override findById() method with for example : + * + *

+ * 
+   {@literal @}Override
+   public Sample findById({@literal @}PathVariable String id) {
+        Sample sample = this.repository.findByName(id);
+        if (sample == null) {
+            throw new NotFoundException();
+        }
+        return sample;
+   }
+   
+ * 
+ * + * + * @param Your resource class to manage, maybe an entity or DTO class + * @param Resource id type, usually Long or String + * @param The repository class + * @see ServiceBasedRestController + */ +public abstract class RepositoryBasedRestController + implements RestController { + + protected R repository; + + protected Logger logger = LoggerFactory.getLogger(RepositoryBasedRestController.class); + + /** + * You should override this setter in order to inject your repository with @Inject annotation + * + * @param repository The repository to be injected + */ + public void setRepository(R repository) { + this.repository = repository; + } + + /** + * {@inheritDoc} + */ + @Override + public T create(@RequestBody T resource) { + return (T)this.repository.save(resource); + } + + /** + * {@inheritDoc} + */ + @Override + public T update(@PathVariable ID id, @RequestBody T resource) { + Assert.notNull(id, "id cannot be null"); + + T retrievedResource = this.findById(id); + if (retrievedResource == null) { + throw new NotFoundException(); + } + + return (T)this.repository.save(resource); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterable findAll() { + return repository.findAll(); + } + + /** + * {@inheritDoc} + */ + @Override + public Page findPaginated(@RequestParam(value = "page", required = false, defaultValue = "1") Integer page, + @RequestParam(value = "size", required = false, defaultValue = "10") Integer size, + @RequestParam(value = "direction", required = false, defaultValue = "") String direction, + @RequestParam(value = "properties", required = false) String properties) { + Assert.isTrue(page > 0, "Page index must be greater than 0"); + Assert.isTrue(direction.isEmpty() || direction.equalsIgnoreCase(Sort.Direction.ASC.toString()) || direction.equalsIgnoreCase(Sort.Direction.DESC.toString()), "Direction should be ASC or DESC"); + if(direction.isEmpty()) { + return this.repository.findAll(new PageRequest(page - 1, size)); + } else { + Assert.notNull(properties); + return this.repository.findAll(new PageRequest(page - 1, size, new Sort(Sort.Direction.fromString(direction.toUpperCase()), properties.split(",")))); + } + } + + /** + * {@inheritDoc} + */ + @Override + public T findById(@PathVariable ID id) { + T entity = (T)this.repository.findOne(id); + if (entity == null) { + throw new NotFoundException(); + } + + return entity; + } + + /** + * {@inheritDoc} + */ + @Override + public Iterable findByIds(@RequestParam(value="ids[]") Set ids){ + Assert.notNull(ids, "ids list cannot be null"); + return this.repository.findAll(ids); + } + + /** + * {@inheritDoc} + */ + @Override + public void delete() { + Iterable list = repository.findAll(); + for (T entity : list) { + repository.delete(entity); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void delete(@PathVariable ID id) { + T resource = this.findById(id); + this.repository.delete(resource); + } + +} diff --git a/SpringJava/JPA/src/main/java/org/resthub/web/controller/RestController.java b/SpringJava/JPA/src/main/java/org/resthub/web/controller/RestController.java new file mode 100644 index 0000000..9aa9a26 --- /dev/null +++ b/SpringJava/JPA/src/main/java/org/resthub/web/controller/RestController.java @@ -0,0 +1,119 @@ +package org.resthub.web.controller; + +import org.resthub.common.exception.NotFoundException; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.io.Serializable; +import java.util.Set; + +/** + * REST controller interface + * + * @param Your resource POJO to manage, maybe an entity or DTO class + * @param Primary resource identifier at webservice level, usually Long or String + */ +public interface RestController { + + /** + * Create a new resource
+ * REST webservice published : POST / + * + * @param resource The resource to create + * @return CREATED http status code if the request has been correctly processed, with updated resource enclosed in the body, usually with and additional identifier automatically created by the database + */ + @RequestMapping(method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + @ResponseBody + T create(@RequestBody T resource); + + /** + * Update an existing resource
+ * REST webservice published : PUT /{id} + * + * @param id The identifier of the resource to update, usually a Long or String identifier. It is explicitely provided in order to handle cases where the identifier could be changed. + * @param resource The resource to update + * @return OK http status code if the request has been correctly processed, with the updated resource enclosed in the body + * @throws NotFoundException + */ + @RequestMapping(value = "{id}", method = RequestMethod.PUT) + @ResponseBody + T update(@PathVariable ID id, @RequestBody T resource); + + /** + * Find all resources, and return the full collection (plain list not paginated)
+ * REST webservice published : GET /?page=no + * + * @return OK http status code if the request has been correctly processed, with the list of all resource enclosed in the body. + * Be careful, this list should be big since it will return ALL resources. In this case, consider using paginated findAll method instead. + */ + @RequestMapping(method = RequestMethod.GET, params = "page=no") + @ResponseBody + Iterable findAll(); + + /** + * Find all resources, and return a paginated and optionaly sorted collection
+ * REST webservice published : GET /search?page=0&size=20 or GET /search?page=0&size=20&direction=desc&properties=name + * + * @param page Page number starting from 0. default to 0 + * @param size Number of resources by pages. default to 10 + * @param direction Optional sort direction, could be "asc" or "desc" + * @param properties Ordered list of comma separeted properies used for sorting resulats. At least one property should be provided if direction is specified + * @return OK http status code if the request has been correctly processed, with the a paginated collection of all resource enclosed in the body. + */ + @RequestMapping(method = RequestMethod.GET) + @ResponseBody + Page findPaginated(@RequestParam(value = "page", required = false, defaultValue = "1") Integer page, + @RequestParam(value = "size", required = false, defaultValue = "10") Integer size, + @RequestParam(value = "direction", required = false, defaultValue = "ASC") String direction, + @RequestParam(value = "properties", required = false) String properties); + + /** + * Find a resource by its identifier
+ * REST webservice published : GET /{id} + * + * @param id The identifier of the resouce to find + * @return OK http status code if the request has been correctly processed, with resource found enclosed in the body + * @throws NotFoundException + */ + @RequestMapping(value = "{id}", method = RequestMethod.GET) + @ResponseBody + T findById(@PathVariable ID id); + + /** + * Find multiple resources by their identifiers
+ * REST webservice published : GET /?ids[]= + *

+ * example : /?ids[]=1&ids[]=2&ids[]=3 + * + * @param ids List of ids to retrieve + * @return OK http status code with list of retrieved resources. Not found resources are ignored: + * no Exception thrown. List is empty if no resource found with any of the given ids. + */ + @RequestMapping(method = RequestMethod.GET, params = "ids[]") + @ResponseBody + Iterable findByIds(@RequestParam(value = "ids[]") Set ids); + + /** + * Delete all resources
+ * REST webservice published : DELETE /
+ * Return No Content http status code if the request has been correctly processed + */ + @RequestMapping(method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.NO_CONTENT) + void delete(); + + /** + * Delete a resource by its identifier
+ * REST webservice published : DELETE /{id}
+ * Return No Content http status code if the request has been correctly processed + * + * @param id The identifier of the resource to delete + * @throws NotFoundException + */ + @RequestMapping(value = "{id}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.NO_CONTENT) + void delete(@PathVariable ID id); + +} diff --git a/SpringJava/JPA/src/main/resources/jpa.properties b/SpringJava/JPA/src/main/resources/jpa.properties index f0755b8..9f628d3 100644 --- a/SpringJava/JPA/src/main/resources/jpa.properties +++ b/SpringJava/JPA/src/main/resources/jpa.properties @@ -1,2 +1,2 @@ -jpa.dialect=org.hibernate.dialect.DB2Dialect -jpa.show_sql=true \ No newline at end of file +jpa.dialect=org.hibernate.dialect.MySQL5Dialect +jpa.show_sql=false \ No newline at end of file diff --git a/SpringJava/JPA/src/main/resources/log4j2.xml b/SpringJava/JPA/src/main/resources/log4j2.xml index d684e48..eec10be 100644 --- a/SpringJava/JPA/src/main/resources/log4j2.xml +++ b/SpringJava/JPA/src/main/resources/log4j2.xml @@ -26,20 +26,33 @@ - - - - + + + + + + + + - - - - + + + + diff --git a/SpringJava/JPA/src/main/resources/spring-configuration/jpa-configuration.xml b/SpringJava/JPA/src/main/resources/spring-configuration/jpa-configuration.xml index 4381156..286d067 100644 --- a/SpringJava/JPA/src/main/resources/spring-configuration/jpa-configuration.xml +++ b/SpringJava/JPA/src/main/resources/spring-configuration/jpa-configuration.xml @@ -15,7 +15,7 @@ - + @@ -28,11 +28,11 @@ - + diff --git a/SpringJava/JPA/src/main/resources/spring-configuration/mvc/rest/rest-config.xml b/SpringJava/JPA/src/main/resources/spring-configuration/mvc/rest/rest-config.xml new file mode 100644 index 0000000..1c2f309 --- /dev/null +++ b/SpringJava/JPA/src/main/resources/spring-configuration/mvc/rest/rest-config.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SpringJava/JPA/src/main/webapp/WEB-INF/web.xml b/SpringJava/JPA/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..01de2a8 --- /dev/null +++ b/SpringJava/JPA/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,41 @@ + + + + Spring JPA: example JPA + + + + org.springframework.web.context.ContextLoaderListener + + + + + spring.profiles.default + ${environment.profile} + contextConfigLocation + + classpath*:spring-configuration/*.xml + + + + + + spring-rest + org.springframework.web.servlet.DispatcherServlet + 1 + true + + contextConfigLocation + classpath*:spring-configuration/mvc/rest/*.xml + + + + + spring-rest + + /* + + + diff --git a/SpringJava/JPA/src/test/java/de/spring/persistence/example/domain/AdTest.java b/SpringJava/JPA/src/test/java/de/spring/persistence/example/domain/AdTest.java new file mode 100644 index 0000000..7fb4c57 --- /dev/null +++ b/SpringJava/JPA/src/test/java/de/spring/persistence/example/domain/AdTest.java @@ -0,0 +1,5 @@ +package de.spring.persistence.example.domain; + +public class AdTest { + +} -- 2.1.4