From 3e2a79d3299f45c1c866b5cfd3abfb1002ca1656 Mon Sep 17 00:00:00 2001 From: Jon Brisbin Date: Wed, 13 Jun 2012 09:23:58 -0500 Subject: [PATCH] Added wiki documentation on API usage from curl. Also fixed a couple bugs, tweaked how rels are generated in some links. --- build.gradle | 2 +- doc/curl_usage.md | 43 +++++++++++++++++ .../ValidatingRepositoryEventListener.java | 14 +++--- .../rest/webmvc/RepositoryRestController.java | 47 ++++++++++++++----- 4 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 doc/curl_usage.md diff --git a/build.gradle b/build.gradle index 41b9c9326..4bd302137 100644 --- a/build.gradle +++ b/build.gradle @@ -74,7 +74,7 @@ configure(subprojects) { subproject -> // Testing testCompile "org.spockframework:spock-core:$spockVersion" testCompile "org.spockframework:spock-spring:$spockVersion" - testCompile "org.hamcrest:hamcrest-all:1.1" + testCompile "org.hamcrest:hamcrest-library:1.1" testCompile "org.springframework:spring-test:$springVersion" testRuntime "org.springframework:spring-context-support:$springVersion" diff --git a/doc/curl_usage.md b/doc/curl_usage.md new file mode 100644 index 000000000..d42a4fc75 --- /dev/null +++ b/doc/curl_usage.md @@ -0,0 +1,43 @@ +# Example API usage with curl + +Here is some example usage of the REST API with `curl`. First we'll add a `Family`: + + $ curl -v -d '{"surname" : "Doe"}' -H "Content-Type: application/json" http://localhost:8080/family + + HTTP/1.1 201 Created + Location: http://localhost:8080/family/1 + Content-Length: 0 + +Now we'll add a `Person`: + + $ curl -v -d '{"name" : "John Doe"}' -H "Content-Type: application/json" http://localhost:8080/people + + HTTP/1.1 201 Created + Location: http://localhost:8080/people/1 + Content-Length: 0 + +Now we'll add this person to the "Doe" family we added above: + + $ curl -v -d 'http://localhost:8080/people/1' -H "Content-Type: text/uri-list" http://localhost:8080/family/1/members + + HTTP/1.1 201 Created + Content-Length: 0 + +Notice that we don't return a `Location` when we add items to a referenced collection because we can add N numbers of items (here we're just adding one) so the `Location` header wouldn't be very meaningful as you couldn't match which URL you POSTed with the corresponding URL in the header. + +Now that we have some links created, let's query them so our user agent can keep track of them: + + $ curl -v http://localhost:8080/family/1/members + + HTTP/1.1 200 OK + Content-Type: application/json;charset=ISO-8859-1 + Content-Length: 118 + + { + "_links" : [ { + "rel" : "family.Family.Person.1", + "href" : "http://localhost:8080/family/1/members/1" + } ] + } + +We can continue adding other top-level entities by sending JSON data and can add links to referenced entities by sending `text/uri-list` data with the URIs to those other top-level entities. diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java index 0ffdbbae0..ba0f33ff9 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java @@ -107,19 +107,19 @@ public class ValidatingRepositoryEventListener validate("afterDelete", entity); } - private Errors validate(String event, Object entity) { + private Errors validate(String event, Object o) { Errors errors = null; - if (null != entity) { - Class domainType = entity.getClass(); + if (null != o) { + Class domainType = o.getClass(); errors = new ValidationErrors(domainType.getSimpleName(), - entity, + o, repositoryMetadataFor(domainType).entityMetadata()); Collection validators = this.validators.get(event); if (null != validators) { for (Validator v : validators) { - if (v.supports(entity.getClass())) { - LOG.debug(event + ": " + entity + " with " + v); - ValidationUtils.invokeValidator(v, entity, errors); + if (v.supports(o.getClass())) { + LOG.debug(event + ": " + o + " with " + v); + ValidationUtils.invokeValidator(v, o, errors); } } } 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 b94a008d7..fa74ab05f 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 @@ -20,6 +20,8 @@ import java.util.concurrent.atomic.AtomicReference; import javax.servlet.http.HttpServletRequest; import org.codehaus.jackson.map.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.InitializingBean; @@ -93,6 +95,8 @@ public class RepositoryRestController public static final String SELF = "self"; public static final String LINKS = "_links"; + private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class); + private ApplicationContext applicationContext; private MediaType uriListMediaType = MediaType.parseMediaType("text/uri-list"); @@ -269,7 +273,7 @@ public class RepositoryRestController while (iter.hasNext()) { Object o = iter.next(); Serializable id = (Serializable) repoMeta.entityMetadata().idAttribute().get(o); - links.add(new SimpleLink(repoMeta.rel() + "." + o.getClass().getSimpleName(), + links.add(new SimpleLink(repoMeta.rel() + "." + o.getClass().getSimpleName() + "." + id.toString(), buildUri(baseUri, repository, id.toString()))); } links.add(new SimpleLink(repoMeta.rel() + ".search", @@ -591,23 +595,27 @@ public class RepositoryRestController ) public ModelAndView deleteEntity(@PathVariable String repository, @PathVariable String id) { + Map model = new HashMap(); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); Serializable serId = stringToSerializable(id, (Class) repoMeta.entityMetadata() .idAttribute() .type()); CrudRepository repo = repoMeta.repository(); - - if (null != applicationContext) { - applicationContext.publishEvent(new BeforeDeleteEvent(serId)); - } - repo.delete(serId); - if (null != applicationContext) { - applicationContext.publishEvent(new AfterDeleteEvent(serId)); + Object entity = repo.findOne(serId); + if (null == entity) { + model.put(STATUS, HttpStatus.NOT_FOUND); + } else { + if (null != applicationContext) { + applicationContext.publishEvent(new BeforeDeleteEvent(entity)); + } + repo.delete(serId); + if (null != applicationContext) { + applicationContext.publishEvent(new AfterDeleteEvent(entity)); + } + model.put(STATUS, HttpStatus.NO_CONTENT); } - Map model = new HashMap(); - model.put(STATUS, HttpStatus.NO_CONTENT); return new ModelAndView(viewName("empty"), model); } @@ -656,7 +664,10 @@ public class RepositoryRestController if (propVal instanceof Collection) { for (Object o : (Collection) propVal) { String propValId = idAttr.get(o).toString(); - String rel = repository + "." + entity.getClass().getSimpleName() + "." + attrType.getSimpleName(); + String rel = repository + "." + + entity.getClass().getSimpleName() + "." + + attrType.getSimpleName() + "." + + propValId; URI path = buildUri(baseUri, repository, id, property, propValId); links.add(new SimpleLink(rel, path)); } @@ -998,6 +1009,7 @@ public class RepositoryRestController @ExceptionHandler(OptimisticLockingFailureException.class) @ResponseBody public ResponseEntity handleLockingFailure(OptimisticLockingFailureException ex) throws IOException { + LOG.error(ex.getMessage(), ex); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); Map m = new HashMap(); @@ -1008,6 +1020,7 @@ public class RepositoryRestController @SuppressWarnings({"unchecked"}) @ExceptionHandler(RepositoryConstraintViolationException.class) public Model handleValidationFailure(RepositoryConstraintViolationException ex) throws IOException { + LOG.error(ex.getMessage(), ex); Model model = new ExtendedModelMap(); model.addAttribute(STATUS, HttpStatus.BAD_REQUEST); @@ -1022,6 +1035,18 @@ public class RepositoryRestController return model; } + @SuppressWarnings({"unchecked"}) + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseBody + public ResponseEntity handleMessageConversionFailure(HttpMessageNotReadableException ex) throws IOException { + LOG.error(ex.getMessage(), ex); + 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(); }