diff --git a/build.gradle b/build.gradle index 14c2bfd02..c5f08d132 100644 --- a/build.gradle +++ b/build.gradle @@ -9,8 +9,8 @@ ext { logbackVersion = "1.0.11" // Spring - springVersion = "3.2.5.RELEASE" - hateoasVersion = "0.8.0.RELEASE" + springVersion = "3.2.6.RELEASE" + hateoasVersion = "0.9.0.BUILD-SNAPSHOT" springPluginVersion = "0.8.0.RELEASE" springSecurityVersion = "3.1.3.RELEASE" sdCommonsVersion = "1.7.0.M1" @@ -20,7 +20,7 @@ ext { sdNeo4jVersion = "3.0.0.M1" // Libraries - jacksonVersion = "2.2.2" + jacksonVersion = "2.3.0" jodaVersion = "2.1" hibernateVersion = "4.2.0.Final" hibernateValidatorVersion = "4.3.1.Final" @@ -361,7 +361,7 @@ configure(rootProject) { task wrapper(type: Wrapper) { description = "Generates gradlew[.bat] scripts" - gradleVersion = "1.8" + gradleVersion = "1.10" doLast() { def gradleOpts = "-XX:MaxPermSize=1024m -Xmx1024m" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a7634b071..d5c591c9c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f82a2054..7e010bda6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Nov 12 18:38:36 CET 2013 +#Tue Dec 17 11:32:11 CET 2013 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.8-bin.zip +distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-bin.zip diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java index 062376642..b493b720d 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.util.Assert; @@ -36,7 +37,7 @@ public class RepositoryRestConfiguration { private String pageParamName = "page"; private String limitParamName = "limit"; private String sortParamName = "sort"; - private MediaType defaultMediaType = MediaType.APPLICATION_JSON; + private MediaType defaultMediaType = MediaTypes.HAL_JSON; private boolean returnBodyOnCreate = false; private boolean returnBodyOnUpdate = false; private List> exposeIdsFor = new ArrayList>(); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java index aa9878571..979a0cb37 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java @@ -54,8 +54,7 @@ public class RepositoryController extends AbstractRepositoryRestController { * @return */ @ResponseBody - @RequestMapping(value = "/", method = RequestMethod.GET, // - produces = { "application/json", "application/x-spring-data-compact+json" }) + @RequestMapping(value = "/", method = RequestMethod.GET) public RepositoryLinksResource listRepositories() { RepositoryLinksResource resource = new RepositoryLinksResource(); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index 4c754ef41..32387427d 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -105,8 +105,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem } @ResponseBody - @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/json", - "application/x-spring-data-verbose+json" }) + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public Resources listEntities(final RepositoryRestRequest request, Pageable pageable, Sort sort) throws ResourceNotFoundException { @@ -158,8 +157,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem } @ResponseBody - @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST, consumes = { "application/json" }, produces = { - "application/json", "text/uri-list" }) + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST, consumes = { "application/json" }) public ResponseEntity createNewEntity(RepositoryRestRequest repoRequest, PersistentEntityResource incoming) { @@ -189,9 +187,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem * @return * @throws ResourceNotFoundException */ - @ResponseBody - @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET, produces = { "application/json", - "application/x-spring-data-compact+json", "text/uri-list" }) + @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET) public ResponseEntity> getSingleEntity(RepositoryRestRequest repoRequest, @PathVariable String id) { RepositoryInvoker repoMethodInvoker = repoRequest.getRepositoryInvoker(); @@ -217,9 +213,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem * @param id * @return */ - @ResponseBody - @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT, consumes = { "application/json" }, - produces = { "application/json", "text/uri-list" }) + @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT, consumes = { "application/json" }) public ResponseEntity updateEntity(RepositoryRestRequest request, PersistentEntityResource incoming, @PathVariable String id) { @@ -251,7 +245,6 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem } @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.DELETE) - @ResponseBody public ResponseEntity deleteEntity(final RepositoryRestRequest repoRequest, @PathVariable final String id) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryLinksResource.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryLinksResource.java index a9f0e40db..97c881943 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryLinksResource.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryLinksResource.java @@ -12,5 +12,4 @@ public class RepositoryLinksResource extends Resources { public RepositoryLinksResource() { super(Collections.emptyList()); } - } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index 1f33bfb35..70c155e09 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -96,9 +96,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes this.publisher = applicationEventPublisher; } - @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/json", - "application/x-spring-data-verbose+json" }) - @ResponseBody + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET) public ResponseEntity followPropertyReference(final RepositoryRestRequest repoRequest, @PathVariable String id, @PathVariable String property) throws ResourceNotFoundException, NoSuchMethodException { @@ -147,7 +145,6 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } @RequestMapping(value = BASE_MAPPING, method = RequestMethod.DELETE) - @ResponseBody public ResponseEntity deletePropertyReference(final RepositoryRestRequest repoRequest, @PathVariable String id, @PathVariable String property) throws ResourceNotFoundException, NoSuchMethodException, HttpRequestMethodNotSupportedException { @@ -193,7 +190,6 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); } - @ResponseBody @RequestMapping(value = BASE_MAPPING + "/{propertyId}", method = RequestMethod.GET, produces = { "application/json", "application/x-spring-data-verbose+json", "application/x-spring-data-compact+json", "text/uri-list" }) public ResponseEntity followPropertyReference(final RepositoryRestRequest repoRequest, @@ -246,7 +242,6 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes return ControllerUtils.toResponseEntity(headers, responseResource, HttpStatus.OK); } - @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/x-spring-data-compact+json", "text/uri-list" }) public ResponseEntity followPropertyReferenceCompact(RepositoryRestRequest repoRequest, diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 0b15b6667..c5d59b431 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -21,7 +21,6 @@ import java.util.Collections; import java.util.List; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; @@ -61,8 +60,15 @@ import org.springframework.data.rest.webmvc.support.ValidationExceptionHandler; import org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.ResourceProcessor; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.core.EvoInflectorRelProvider; +import org.springframework.hateoas.hal.CurieProvider; +import org.springframework.hateoas.hal.Jackson2HalModule; +import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; @@ -89,6 +95,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; @Configuration @ComponentScan(basePackageClasses = RepositoryRestController.class, includeFilters = @Filter(RepositoryRestController.class), useDefaultFilters = false) +@EnableHypermediaSupport(type = HypermediaType.HAL) @ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml") public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebConfiguration { @@ -99,6 +106,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Autowired ListableBeanFactory beanFactory; @Autowired(required = false) List> resourceProcessors = Collections.emptyList(); + @Autowired(required = false) RelProvider relProvider; + @Autowired(required = false) CurieProvider curieProvider; @Bean public Repositories repositories() { @@ -256,15 +265,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public ObjectMapper objectMapper() { - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); - // Our special PersistentEntityResource Module - objectMapper.registerModule(persistentEntityJackson2Module()); - Jackson2DatatypeHelper.configureObjectMapper(objectMapper); - // Configure custom Modules - configureJacksonObjectMapper(objectMapper); - - return objectMapper; + return basicObjectMapper(); } /** @@ -274,14 +275,58 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public MappingJackson2HttpMessageConverter jacksonHttpMessageConverter() { + + List mediaTypes = new ArrayList(); + mediaTypes.addAll(Arrays.asList(MediaType.valueOf("application/schema+json"), + MediaType.valueOf("application/x-spring-data-verbose+json"), + MediaType.valueOf("application/x-spring-data-compact+json"))); + + // Configure this mapper to be used if HAL is not the default media type + if (!config().getDefaultMediaType().equals(MediaTypes.HAL_JSON)) { + mediaTypes.add(MediaType.APPLICATION_JSON); + } + MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(); jacksonConverter.setObjectMapper(objectMapper()); - jacksonConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON, - MediaType.valueOf("application/schema+json"), MediaType.valueOf("application/x-spring-data-verbose+json"), - MediaType.valueOf("application/x-spring-data-compact+json"))); + jacksonConverter.setSupportedMediaTypes(mediaTypes); + return jacksonConverter; } + // + // HAL setup + // + + @Bean + public MappingJackson2HttpMessageConverter halJacksonHttpMessageConverter() { + + ArrayList mediaTypes = new ArrayList(); + mediaTypes.add(MediaTypes.HAL_JSON); + + // Enable returning HAL if application/json is asked if it's configured to be the default type + if (config().getDefaultMediaType().equals(MediaTypes.HAL_JSON)) { + mediaTypes.add(MediaType.APPLICATION_JSON); + } + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setObjectMapper(halObjectMapper()); + converter.setSupportedMediaTypes(mediaTypes); + + return converter; + } + + @Bean + public ObjectMapper halObjectMapper() { + + HalHandlerInstantiator instantiator = new HalHandlerInstantiator(getDefaultedRelProvider(), curieProvider); + + ObjectMapper mapper = basicObjectMapper(); + mapper.registerModule(new Jackson2HalModule()); + mapper.setHandlerInstantiator(instantiator); + + return mapper; + } + /** * The {@link HttpMessageConverter} used to create {@literal text/uri-list} responses. * @@ -338,12 +383,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon Repositories repositories = repositories(); RepositoryRestConfiguration config = config(); - try { - RelProvider relProvider = beanFactory.getBean(RelProvider.class); - return new ResourceMappings(config, repositories, relProvider); - } catch (NoSuchBeanDefinitionException e) { - return new ResourceMappings(config, repositories); - } + return new ResourceMappings(config, repositories, getDefaultedRelProvider()); } /** @@ -381,9 +421,13 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon } private List> defaultMessageConverters() { + List> messageConverters = new ArrayList>(); + + messageConverters.add(halJacksonHttpMessageConverter()); messageConverters.add(jacksonHttpMessageConverter()); messageConverters.add(uriListHttpMessageConverter()); + return messageConverters; } @@ -393,6 +437,23 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon resourceMetadataHandlerMethodArgumentResolver()); } + private ObjectMapper basicObjectMapper() { + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); + // Our special PersistentEntityResource Module + objectMapper.registerModule(persistentEntityJackson2Module()); + Jackson2DatatypeHelper.configureObjectMapper(objectMapper); + // Configure custom Modules + configureJacksonObjectMapper(objectMapper); + + return objectMapper; + } + + private RelProvider getDefaultedRelProvider() { + return this.relProvider != null ? relProvider : new EvoInflectorRelProvider(); + } + /** * Override this method to add additional configuration. * diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index 4d56cb7ed..e14c79938 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -31,9 +32,12 @@ import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.webmvc.PersistentEntityResource; import org.springframework.data.rest.webmvc.support.RepositoryLinkBuilder; import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.util.Assert; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -54,11 +58,13 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init private static final long serialVersionUID = -7289265674870906323L; private static final Logger LOG = LoggerFactory.getLogger(PersistentEntityJackson2Module.class); private static final TypeDescriptor URI_TYPE = TypeDescriptor.valueOf(URI.class); + + private final ResourceMappings mappings; + private final ConversionService conversionService; + @Autowired private Repositories repositories; @Autowired private RepositoryRestConfiguration config; @Autowired private UriDomainClassConverter uriDomainClassConverter; - private final ResourceMappings mappings; - private final ConversionService conversionService; public PersistentEntityJackson2Module(ResourceMappings resourceMappings, ConversionService conversionService) { @@ -123,11 +129,10 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init @SuppressWarnings({ "unchecked", "incomplete-switch", "unused" }) @Override public T deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { - Object entity = instantiateClass(getValueClass()); + Object entity = instantiateClass(handledType()); BeanWrapper wrapper = BeanWrapper.create(entity, conversionService); - - ResourceMetadata metadata = mappings.getMappingFor(getValueClass()); + ResourceMetadata metadata = mappings.getMappingFor(handledType()); for (JsonToken tok = jp.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) { String name = jp.getCurrentName(); @@ -256,14 +261,14 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init final PersistentEntity entity = resource.getPersistentEntity(); final BeanWrapper, Object> wrapper = BeanWrapper.create(obj, null); final Object entityId = wrapper.getProperty(entity.getIdProperty()); - final ResourceMappings mappings = new ResourceMappings(config, repositories); final ResourceMetadata metadata = mappings.getMappingFor(entity.getType()); final RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, config.getBaseUri()).slash(entityId); final List links = new ArrayList(); // Start with ResourceProcessor-added links links.addAll(resource.getLinks()); - jgen.writeStartObject(); + + final Map model = new LinkedHashMap(); try { @@ -284,12 +289,7 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init } // Property is a normal or non-managed property. - Object propertyValue = wrapper.getProperty(property); - try { - jgen.writeObjectField(property.getName(), propertyValue); - } catch (IOException e) { - throw new IllegalStateException(e); - } + model.put(property.getName(), wrapper.getProperty(property)); } }); @@ -310,28 +310,45 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init } // Association Link was not added, probably because this isn't a managed type. Add value of property inline. - Object propertyValue = wrapper.getProperty(property); - try { - jgen.writeObjectField(property.getName(), propertyValue); - } catch (IOException e) { - throw new IllegalStateException(e); + if (metadata.isExported(property)) { + model.put(property.getName(), wrapper.getProperty(property)); } + } }); - jgen.writeArrayFieldStart("links"); - - for (Link l : links) { - jgen.writeObject(l); - } - - jgen.writeEndArray(); + MapResource mapResource = new MapResource(model, links); + jgen.writeObject(mapResource); } catch (IllegalStateException e) { throw (IOException) e.getCause(); - } finally { - jgen.writeEndObject(); } } } + + private static class MapResource extends Resource> { + + /** + * @param content + * @param links + */ + public MapResource(Map content, Iterable links) { + super(content, links); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.Resource#getContent() + */ + @Override + @JsonIgnore + public Map getContent() { + return super.getContent(); + } + + @JsonAnyGetter + public Map any() { + return getContent(); + } + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java index 61a0b3f53..cebc410b1 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java @@ -29,7 +29,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkDiscoverer; -import org.springframework.hateoas.core.DefaultLinkDiscoverer; +import org.springframework.hateoas.LinkDiscoverers; +import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.context.ContextConfiguration; @@ -52,20 +53,24 @@ import com.jayway.jsonpath.JsonPath; @ContextConfiguration(classes = RepositoryRestMvcConfiguration.class) public abstract class AbstractWebIntegrationTests { + protected static MediaType DEFAULT_MEDIA_TYPE = org.springframework.hateoas.MediaTypes.HAL_JSON; + @Autowired WebApplicationContext context; + @Autowired LinkDiscoverers discoverers; protected MockMvc mvc; - LinkDiscoverer links = new DefaultLinkDiscoverer(); @Before public void setUp() { - mvc = MockMvcBuilders.webAppContextSetup(context).build(); + + mvc = MockMvcBuilders.webAppContextSetup(context).// + defaultRequest(get("/").accept(DEFAULT_MEDIA_TYPE)).build(); } protected MockHttpServletResponse request(String href, MediaType contentType) throws Exception { return mvc.perform(get(href).accept(contentType)). // andExpect(status().isOk()). // - andExpect(content().contentType(MediaType.APPLICATION_JSON)). // + andExpect(content().contentType(contentType)). // andReturn().getResponse(); } @@ -74,7 +79,7 @@ public abstract class AbstractWebIntegrationTests { } protected MockHttpServletResponse request(String href) throws Exception { - return request(href, MediaType.APPLICATION_JSON); + return request(href, DEFAULT_MEDIA_TYPE); } protected ResultActions follow(Link link) throws Exception { @@ -93,21 +98,30 @@ public abstract class AbstractWebIntegrationTests { } protected List discover(Link root, String rel) throws Exception { - String s = mvc.perform(get(root.getHref())).andExpect(status().isOk()).andExpect(hasLinkWithRel(rel)).andReturn() - .getResponse().getContentAsString(); - return links.findLinksWithRel(rel, s); + + MockHttpServletResponse response = mvc.perform(get(root.getHref()).accept(DEFAULT_MEDIA_TYPE)).// + andExpect(status().isOk()).// + andExpect(hasLinkWithRel(rel)).// + andReturn().getResponse(); + + String s = response.getContentAsString(); + return getDiscoverer(response).findLinksWithRel(rel, s); } protected Link discoverUnique(Link root, String rel) throws Exception { - String s = mvc.perform(get(root.getHref())).andExpect(status().isOk()).andExpect(hasLinkWithRel(rel)).andReturn() - .getResponse().getContentAsString(); - return links.findLinkWithRel(rel, s); + + MockHttpServletResponse response = mvc.perform(get(root.getHref()).accept(DEFAULT_MEDIA_TYPE)).// + andExpect(status().isOk()).// + andExpect(hasLinkWithRel(rel)).// + andReturn().getResponse(); + + return assertHasLinkWithRel(rel, response); } protected Link assertHasLinkWithRel(String rel, MockHttpServletResponse response) throws Exception { String content = response.getContentAsString(); - Link link = links.findLinkWithRel(rel, content); + Link link = getDiscoverer(response).findLinkWithRel(rel, content); assertThat("Expected to find link with rel " + rel + " but found none in " + content + "!", link, is(notNullValue())); @@ -117,8 +131,8 @@ public abstract class AbstractWebIntegrationTests { protected Link assertHasContentLinkWithRel(String rel, MockHttpServletResponse response) throws Exception { - String href = JsonPath - .read(response.getContentAsString(), String.format("$..links[?(@.rel == '%s')].href[0]", rel)).toString(); + String content = response.getContentAsString(); + String href = JsonPath.read(content, String.format("$.._links.%s.href[0]", rel)).toString(); assertThat("Expected to find a link with rel" + rel + " in the content section of the response!", href, is(notNullValue())); @@ -128,23 +142,43 @@ public abstract class AbstractWebIntegrationTests { protected void assertDoesNotHaveLinkWithRel(String rel, MockHttpServletResponse response) throws Exception { String content = response.getContentAsString(); - Link link = links.findLinkWithRel(rel, content); + Link link = getDiscoverer(response).findLinkWithRel(rel, content); assertThat("Expected not to find link with rel " + rel + " but found " + link + "!", link, is(nullValue())); } - protected void assertHasJsonPathValue(String path, MockHttpServletResponse response) throws Exception { - assertThat(JsonPath.read(response.getContentAsString(), path), is(notNullValue())); + private LinkDiscoverer getDiscoverer(MockHttpServletResponse response) { + + String contentType = response.getContentType(); + LinkDiscoverer linkDiscovererFor = discoverers.getLinkDiscovererFor(contentType); + + assertThat("Did not find a LinkDiscoverer for returned media type " + contentType + "!", linkDiscovererFor, + is(notNullValue())); + + return linkDiscovererFor; + } + + @SuppressWarnings("unchecked") + protected T assertHasJsonPathValue(String path, MockHttpServletResponse response) throws Exception { + + Object jsonPathResult = JsonPath.read(response.getContentAsString(), path); + assertThat(jsonPathResult, is(notNullValue())); + + return (T) jsonPathResult; } protected ResultMatcher hasLinkWithRel(final String rel) { return new ResultMatcher() { + @Override public void match(MvcResult result) throws Exception { - String s = result.getResponse().getContentAsString(); - assertThat("Expected to find link with rel " + rel + " but found none in " + s, links.findLinkWithRel(rel, s), - notNullValue()); + + MockHttpServletResponse response = result.getResponse(); + String s = response.getContentAsString(); + + assertThat("Expected to find link with rel " + rel + " but found none in " + s, // + getDiscoverer(response).findLinkWithRel(rel, s), notNullValue()); } }; } @@ -155,17 +189,22 @@ public abstract class AbstractWebIntegrationTests { @Override public void match(MvcResult result) throws Exception { - String s = result.getResponse().getContentAsString(); - assertThat("Expected not to find link with rel " + rel + " but found one in " + s, - links.findLinkWithRel(rel, s), nullValue()); + + MockHttpServletResponse response = result.getResponse(); + String s = response.getContentAsString(); + + assertThat("Expected not to find link with rel " + rel + " but found one in " + s, // + getDiscoverer(response).findLinkWithRel(rel, s), nullValue()); } }; } + // Root test cases + @Test public void exposesRootResource() throws Exception { - ResultActions actions = mvc.perform(get("/")).andExpect(status().isOk()); + ResultActions actions = mvc.perform(get("/").accept(DEFAULT_MEDIA_TYPE)).andExpect(status().isOk()); for (String rel : expectedRootLinkRels()) { actions.andExpect(hasLinkWithRel(rel)); @@ -194,5 +233,27 @@ public abstract class AbstractWebIntegrationTests { } } + /** + * @see DATAREST-203 + */ + @Test + public void servesHalWhenRequested() throws Exception { + + mvc.perform(get("/")). // + andExpect(content().contentType(MediaTypes.HAL_JSON)). // + andExpect(jsonPath("$._links", notNullValue())); + } + + /** + * @see DATAREST-203 + */ + @Test + public void servesHalWhenJsonIsRequested() throws Exception { + + mvc.perform(get("/").accept(MediaType.APPLICATION_JSON)). // + andExpect(content().contentType(MediaType.APPLICATION_JSON)). // + andExpect(jsonPath("$._links", notNullValue())); + } + protected abstract Iterable expectedRootLinkRels(); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java index 50606ec2e..a02930e00 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java @@ -22,8 +22,12 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.data.rest.webmvc.RepositoryLinksResource; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.hateoas.core.DefaultRelProvider; +import org.springframework.http.converter.HttpMessageConverter; + +import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Oliver Gierke @@ -45,8 +49,14 @@ public class RepositoryRestMvConfigurationIntegrationTests { } @Test - public void foo() { + public void assertBeansBeingSetUp() throws Exception { + context.getBean(PageableHandlerMethodArgumentResolver.class); + + // Verify HAL setup + context.getBean("halJacksonHttpMessageConverter", HttpMessageConverter.class); + ObjectMapper mapper = context.getBean("halObjectMapper", ObjectMapper.class); + mapper.writeValueAsString(new RepositoryLinksResource()); } @Configuration diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index 35c87a431..91416907c 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -47,6 +47,8 @@ import org.springframework.util.StringUtils; @ContextConfiguration(classes = JpaRepositoryConfig.class) public class JpaWebTests extends AbstractWebIntegrationTests { + static final String LINK_TO_SIBLINGS_OF = "$._embedded..[?(@.firstName == '%s')]._links.siblings.href[0]"; + @Autowired TestDataPopulator loader; @Autowired ResourceMappings mappings; @@ -139,6 +141,24 @@ public class JpaWebTests extends AbstractWebIntegrationTests { ).andExpect(status().isCreated()); } + @Test + public void listsSiblingsWithContentCorrectly() throws Exception { + + MockHttpServletResponse response = mvc.perform(get("/people")).andReturn().getResponse(); + String href = assertHasJsonPathValue(String.format(LINK_TO_SIBLINGS_OF, "John"), response); + + mvc.perform(get(href)).andExpect(status().isOk()); + } + + @Test + public void listsEmptySiblingsCorrectly() throws Exception { + + MockHttpServletResponse response = mvc.perform(get("/people")).andReturn().getResponse(); + String href = assertHasJsonPathValue(String.format(LINK_TO_SIBLINGS_OF, "Billy Bob"), response); + + mvc.perform(get(href)).andExpect(status().isOk()); + } + private String readFile(String name) throws Exception { ClassPathResource file = new ClassPathResource(name, getClass()); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java index 8627f5a3f..c1ad4929e 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java @@ -18,7 +18,7 @@ import org.springframework.data.rest.webmvc.jpa.Person; import org.springframework.data.rest.webmvc.jpa.PersonRepository; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkDiscoverer; -import org.springframework.hateoas.core.DefaultLinkDiscoverer; +import org.springframework.hateoas.hal.HalLinkDiscoverer; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.transaction.annotation.Transactional; @@ -44,7 +44,7 @@ public class PersistentEntitySerializationTests { @Before public void setUp() { - linkDiscoverer = new DefaultLinkDiscoverer(); + linkDiscoverer = new HalLinkDiscoverer(); } @Test diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java index 47bfc61b3..3bfa71a43 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -16,6 +16,9 @@ import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; import org.springframework.data.rest.webmvc.jpa.Person; import org.springframework.data.rest.webmvc.jpa.PersonRepository; import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.core.EvoInflectorRelProvider; +import org.springframework.hateoas.hal.Jackson2HalModule; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; @@ -77,8 +80,14 @@ public class RepositoryTestsConfig { @Bean public ObjectMapper objectMapper() { + + RelProvider relProvider = new EvoInflectorRelProvider(); ObjectMapper mapper = new ObjectMapper(); + + mapper.registerModule(new Jackson2HalModule()); mapper.registerModule(persistentEntityModule()); + mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null)); + return mapper; } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java index 9a0eb9a2b..eb4ed7a8f 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/mongodb/MongoWebTests.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests; import org.springframework.hateoas.Link; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; /** * Integration tests for MongoDB repositories. @@ -83,7 +84,8 @@ public class MongoWebTests extends AbstractWebIntegrationTests { public void foo() throws Exception { Link profileLink = discoverUnique("profiles"); - follow(profileLink).andExpect(jsonPath("$.content").value(hasSize(2))); + follow(profileLink).// + andExpect(jsonPath("$._embedded.profiles").value(hasSize(2))); } @Test @@ -91,6 +93,8 @@ public class MongoWebTests extends AbstractWebIntegrationTests { Link usersLink = discoverUnique("users"); Link userLink = assertHasContentLinkWithRel("self", request(usersLink)); - follow(userLink).andExpect(jsonPath("$.address.zipCode").value(is(notNullValue()))); + follow(userLink).// + andDo(MockMvcResultHandlers.print()). // + andExpect(jsonPath("$.address.zipCode").value(is(notNullValue()))); } }