diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java index 07568bb22..239f96daa 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/JpaRepositoryMetadata.java @@ -40,6 +40,10 @@ public class JpaRepositoryMetadata implements InitializingBean, ApplicationConte this.metamodel = entityManager.getMetamodel(); } + public EntityManager entityManager() { + return this.entityManager; + } + @SuppressWarnings({"unchecked"}) public CrudRepository repositoryFor(String name) { if (null != name) { diff --git a/spring-data-rest-webmvc/build.gradle b/spring-data-rest-webmvc/build.gradle index 184ee9ec3..c290b97af 100644 --- a/spring-data-rest-webmvc/build.gradle +++ b/spring-data-rest-webmvc/build.gradle @@ -6,6 +6,10 @@ dependencies { // JPA compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final" + // JSR 303 Validation + compile "javax.validation:validation-api:1.0.0.GA" + runtime "org.hibernate:hibernate-validator-annotation-processor:4.1.0.Final" + // Spring compile "org.springframework:spring-webmvc:$springVersion" runtime "cglib:cglib-nodep:2.2.2" diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java index 31c7547f0..2788c06a2 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java @@ -21,11 +21,11 @@ import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.PluralAttribute; import javax.persistence.metamodel.SingularAttribute; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.codehaus.jackson.map.ObjectMapper; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.rest.core.Handler; @@ -39,15 +39,19 @@ import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.server.ServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.UriComponentsBuilder; /** @@ -63,19 +67,13 @@ public class RepositoryRestController implements InitializingBean { public static final String SELF = "self"; public static final String LINKS = "_links"; - public static final int HAS_RESOURCE = 1; - public static final int HAS_RESOURCE_ID = 2; - public static final int HAS_SECOND_LEVEL_RESOURCE = 3; - public static final int HAS_SECOND_LEVEL_ID = 4; - - private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class); - private MediaType uriListMediaType = MediaType.parseMediaType("text/uri-list"); private MediaType jsonMediaType = MediaType.parseMediaType("application/x-spring-data+json"); private JpaRepositoryMetadata repositoryMetadata; private Map typeMetaCache = new ConcurrentHashMap(); private ConversionService conversionService = new DefaultConversionService(); private List> httpMessageConverters; + private ObjectMapper objectMapper = new ObjectMapper(); public JpaRepositoryMetadata getRepositoryMetadata() { return repositoryMetadata; @@ -249,7 +247,7 @@ public class RepositoryRestController implements InitializingBean { public void create(ServerHttpRequest request, UriComponentsBuilder uriBuilder, @PathVariable String repository, - Model model) { + Model model) throws IOException { URI baseUri = uriBuilder.build().toUri(); CrudRepository repo = repositoryMetadata.repositoryFor(repository); @@ -261,25 +259,20 @@ public class RepositoryRestController implements InitializingBean { final TypeMetaCacheEntry typeMeta = typeMetaEntry(repo); MediaType incomingMediaType = request.getHeaders().getContentType(); - try { - final Object incoming = readIncoming(request, incomingMediaType, typeMeta.domainClass); - if (null == incoming) { - model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE); - } else { - Object savedEntity = repo.save(incoming); - String sId = typeMeta.entityInfo.getId(savedEntity).toString(); + final Object incoming = readIncoming(request, incomingMediaType, typeMeta.domainClass); + if (null == incoming) { + model.addAttribute(STATUS, HttpStatus.NOT_ACCEPTABLE); + } else { + Object savedEntity = repo.save(incoming); + String sId = typeMeta.entityInfo.getId(savedEntity).toString(); - URI selfUri = buildUri(baseUri, repository, sId); + URI selfUri = buildUri(baseUri, repository, sId); - HttpHeaders headers = new HttpHeaders(); - headers.set(LOCATION, selfUri.toString()); + HttpHeaders headers = new HttpHeaders(); + headers.set(LOCATION, selfUri.toString()); - model.addAttribute(HEADERS, headers); - model.addAttribute(STATUS, HttpStatus.CREATED); - } - } catch (IOException e) { - model.addAttribute(STATUS, HttpStatus.BAD_REQUEST); - LOG.error(e.getMessage(), e); + model.addAttribute(HEADERS, headers); + model.addAttribute(STATUS, HttpStatus.CREATED); } } @@ -355,7 +348,10 @@ public class RepositoryRestController implements InitializingBean { UriComponentsBuilder uriBuilder, @PathVariable String repository, @PathVariable String id, - Model model) { + Model model) + throws IOException, + IllegalAccessException, + InstantiationException { URI baseUri = uriBuilder.build().toUri(); CrudRepository repo = repositoryMetadata.repositoryFor(repository); @@ -370,17 +366,7 @@ public class RepositoryRestController implements InitializingBean { Object entity = null; switch (request.getMethod()) { case POST: - try { - entity = typeMeta.domainClass.newInstance(); - } catch (InstantiationException e) { - model.addAttribute(STATUS, HttpStatus.INTERNAL_SERVER_ERROR); - LOG.error(e.getMessage(), e); - return; - } catch (IllegalAccessException e) { - model.addAttribute(STATUS, HttpStatus.INTERNAL_SERVER_ERROR); - LOG.error(e.getMessage(), e); - return; - } + entity = typeMeta.domainClass.newInstance(); break; case PUT: entity = repo.findOne(serId); @@ -391,36 +377,23 @@ public class RepositoryRestController implements InitializingBean { model.addAttribute(STATUS, HttpStatus.NOT_FOUND); } else { final MediaType incomingMediaType = request.getHeaders().getContentType(); - try { - final Object incoming = readIncoming(request, incomingMediaType, typeMeta.domainClass); - if (null == incoming) { - model.addAttribute(STATUS, HttpStatus.BAD_REQUEST); + final Object incoming = readIncoming(request, incomingMediaType, typeMeta.domainClass); + if (null == incoming) { + throw new HttpMessageNotReadableException("Could not create an instance of " + typeMeta.domainClass + .getSimpleName() + " from input."); + } else { + typeMeta.entityMetadata.id(serId, incoming); + if (request.getMethod() == HttpMethod.POST) { + repo.save(incoming); + URI selfUri = buildUri(baseUri, repository, id); + HttpHeaders headers = new HttpHeaders(); + headers.set(LOCATION, selfUri.toString()); + model.addAttribute(HEADERS, headers); + model.addAttribute(STATUS, HttpStatus.CREATED); } else { - if (request.getMethod() == HttpMethod.POST) { - typeMeta.entityMetadata.id(serId, incoming); - Object savedEntity = repo.save(entity); - String savedId = typeMeta.entityInfo.getId(savedEntity).toString(); - URI selfUri = buildUri(baseUri, repository, savedId); - HttpHeaders headers = new HttpHeaders(); - headers.set(LOCATION, selfUri.toString()); - model.addAttribute(HEADERS, headers); - model.addAttribute(STATUS, HttpStatus.CREATED); - - } else { - for (Map.Entry entry : typeMeta.entityMetadata.embeddedAttributes().entrySet()) { - String name = entry.getKey(); - Object o = typeMeta.entityMetadata.get(name, incoming); - if (null != o) { - typeMeta.entityMetadata.set(name, o, entity); - } - } - repo.save(entity); - model.addAttribute(STATUS, HttpStatus.NO_CONTENT); - } + repo.save(incoming); + model.addAttribute(STATUS, HttpStatus.NO_CONTENT); } - } catch (IOException e) { - model.addAttribute(STATUS, HttpStatus.BAD_REQUEST); - LOG.error(e.getMessage(), e); } } } @@ -478,7 +451,9 @@ public class RepositoryRestController implements InitializingBean { model.addAttribute(STATUS, HttpStatus.NOT_FOUND); } else { Attribute attr = typeMeta.entityType.getAttribute(property); - if (null != attr) { + if (null == attr) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + } else { Class childType; if (attr instanceof PluralAttribute) { childType = ((PluralAttribute) attr).getElementType().getJavaType(); @@ -527,8 +502,6 @@ public class RepositoryRestController implements InitializingBean { } else { model.addAttribute(STATUS, HttpStatus.NOT_FOUND); } - } else { - model.addAttribute(STATUS, HttpStatus.NOT_FOUND); } } } @@ -554,7 +527,7 @@ public class RepositoryRestController implements InitializingBean { @PathVariable String repository, @PathVariable String id, final @PathVariable String property, - final Model model) { + final Model model) throws IOException { URI baseUri = uriBuilder.build().toUri(); CrudRepository repo = repositoryMetadata.repositoryFor(repository); @@ -571,7 +544,9 @@ public class RepositoryRestController implements InitializingBean { model.addAttribute(STATUS, HttpStatus.NOT_FOUND); } else { final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property); - if (null != attr) { + if (null == attr) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + } else { final AtomicReference rel = new AtomicReference(); Handler entityHandler = new Handler() { @Override public Void handle(Object childEntity) { @@ -624,42 +599,35 @@ public class RepositoryRestController implements InitializingBean { } }; MediaType incomingMediaType = request.getHeaders().getContentType(); - try { - if (uriListMediaType.equals(incomingMediaType)) { - BufferedReader in = new BufferedReader(new InputStreamReader(request.getBody())); - String line; - while (null != (line = in.readLine())) { - String sLinkUri = line.trim(); - Object o = resolveTopLevelResource(baseUri, sLinkUri); - if (null != o) { - entityHandler.handle(o); - } - } - } else if (jsonMediaType.equals(incomingMediaType)) { - final Map>> incoming = readIncoming(request, incomingMediaType, Map.class); - for (Map link : incoming.get(LINKS)) { - String sLinkUri = link.get("href"); - Object o = resolveTopLevelResource(baseUri, sLinkUri); - rel.set(link.get("rel")); - if (null != o) { - entityHandler.handle(o); - } + if (uriListMediaType.equals(incomingMediaType)) { + BufferedReader in = new BufferedReader(new InputStreamReader(request.getBody())); + String line; + while (null != (line = in.readLine())) { + String sLinkUri = line.trim(); + Object o = resolveTopLevelResource(baseUri, sLinkUri); + if (null != o) { + entityHandler.handle(o); } } - - repo.save(entity); - - if (request.getMethod() == HttpMethod.PUT) { - model.addAttribute(STATUS, HttpStatus.NO_CONTENT); - } else { - model.addAttribute(STATUS, HttpStatus.CREATED); + } else if (jsonMediaType.equals(incomingMediaType)) { + final Map>> incoming = readIncoming(request, incomingMediaType, Map.class); + for (Map link : incoming.get(LINKS)) { + String sLinkUri = link.get("href"); + Object o = resolveTopLevelResource(baseUri, sLinkUri); + rel.set(link.get("rel")); + if (null != o) { + entityHandler.handle(o); + } } - } catch (IOException e) { - model.addAttribute(STATUS, HttpStatus.INTERNAL_SERVER_ERROR); - LOG.error(e.getMessage(), e); } - } else { - model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + + repo.save(entity); + + if (request.getMethod() == HttpMethod.PUT) { + model.addAttribute(STATUS, HttpStatus.NO_CONTENT); + } else { + model.addAttribute(STATUS, HttpStatus.CREATED); + } } } } @@ -784,10 +752,14 @@ public class RepositoryRestController implements InitializingBean { model.addAttribute(STATUS, HttpStatus.NOT_FOUND); } else { final Attribute attr = typeMeta.entityMetadata.linkedAttributes().get(property); - if (null != attr) { + if (null == attr) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + } else { // Find child entity CrudRepository childRepo = repositoryFromAttribute(attr); - if (null != childRepo) { + if (null == childRepo) { + model.addAttribute(STATUS, HttpStatus.NOT_FOUND); + } else { TypeMetaCacheEntry childTypeMeta = typeMetaEntry(childRepo); Serializable sChildId = stringToSerializable(childId, childTypeMeta.idType); Object childEntity = childRepo.findOne(sChildId); @@ -832,13 +804,22 @@ public class RepositoryRestController implements InitializingBean { model.addAttribute(STATUS, HttpStatus.NO_CONTENT); } - } else { - model.addAttribute(STATUS, HttpStatus.NOT_FOUND); } } } } + @SuppressWarnings({"unchecked"}) + @ExceptionHandler(OptimisticLockingFailureException.class) + @ResponseBody + public ResponseEntity handleLockingFailure(OptimisticLockingFailureException ex) throws IOException { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + Map m = new HashMap(); + m.put("message", ex.getMessage()); + return new ResponseEntity(objectMapper.writeValueAsBytes(m), headers, HttpStatus.BAD_REQUEST); + } + private static URI buildUri(URI baseUri, String... pathSegments) { return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri(); } @@ -854,7 +835,7 @@ public class RepositoryRestController implements InitializingBean { @SuppressWarnings({"unchecked"}) private CrudRepository repositoryFromAttribute(Attribute attr) { - CrudRepository repo = null; + CrudRepository repo; if (attr instanceof PluralAttribute) { repo = repositoryMetadata.repositoryFor(((PluralAttribute) attr).getElementType().getJavaType()); } else { diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy index 757217cc7..d6f93f106 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy @@ -73,10 +73,18 @@ class RepositoryRestControllerSpec extends Specification { then: model.status == HttpStatus.CREATED + when: "getting specific entity" + model.clear() + req = createRequest("GET", "person/1") + controller.entity(new ServletServerHttpRequest(req), uriBuilder, "person", "1", model) + + then: + model.resource?.name == "John Doe" + when: "updating an entity" model.clear() req = createRequest("PUT", "person/1") - data = mapper.writeValueAsBytes([name: "Johnnie Doe"]) + data = mapper.writeValueAsBytes([name: "Johnnie Doe", version: 0]) req.content = data controller.createOrUpdate(new ServletServerHttpRequest(req), uriBuilder, "person", "1", model) @@ -92,14 +100,6 @@ class RepositoryRestControllerSpec extends Specification { model.status == HttpStatus.OK personsLinks[0].href().toString() == "http://localhost:8080/data/person/1" - when: "getting specific entity" - model.clear() - req = createRequest("GET", "person/1") - controller.entity(new ServletServerHttpRequest(req), uriBuilder, "person", "1", model) - - then: - model.resource?.name == "Johnnie Doe" - when: "creating child entity" model.clear() req = createRequest("POST", "address")