DATAREST-203, DATAREST-75 - Initial support for HAL.

This commit introduces support for HAL rendering and making it the default rendering option. The configuration now registers an additional MappingJackson2HttpMessageConverter with the according HAL modules registered and makes this the default JSON renderer if the default media type is configured to HAL (which it is by default). To change this, set RepositoryRestConfiguration.setDefaultMediaType(…) back to application/json.

Refactored the PersistentEntityJackson2Module to turn the PersistentEntityResource into it's correct shape (turning associations into links) and the delegate to the default rendering, so that the HAL customizations provided by Spring HATEOAS can kick in. Adapted test cases to the change in default representation format.


Upgraded to Jackson 2.3.0, Spring 3.2.6 and Spring HATEOAS 0.9.0.BUILD-SNAPSHOT. Upgraded to Gradle 1.10.
This commit is contained in:
Oliver Gierke
2013-12-19 14:44:45 +01:00
parent 44420d862c
commit af520871dc
16 changed files with 271 additions and 102 deletions

View File

@@ -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"

Binary file not shown.

View File

@@ -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

View File

@@ -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<Class<?>> exposeIdsFor = new ArrayList<Class<?>>();

View File

@@ -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();

View File

@@ -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<ResourceSupport> 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<Resource<?>> 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<? extends ResourceSupport> updateEntity(RepositoryRestRequest request,
PersistentEntityResource<Object> 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 {

View File

@@ -12,5 +12,4 @@ public class RepositoryLinksResource extends Resources<Object> {
public RepositoryLinksResource() {
super(Collections.emptyList());
}
}

View File

@@ -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<ResourceSupport> 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<? extends ResourceSupport> 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<ResourceSupport> 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<ResourceSupport> followPropertyReferenceCompact(RepositoryRestRequest repoRequest,

View File

@@ -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<ResourceProcessor<?>> 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<MediaType> mediaTypes = new ArrayList<MediaType>();
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<MediaType> mediaTypes = new ArrayList<MediaType>();
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<HttpMessageConverter<?>> defaultMessageConverters() {
List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
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.
*

View File

@@ -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<?, Object> 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<PersistentEntity<Object, ?>, 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<Link> links = new ArrayList<Link>();
// Start with ResourceProcessor-added links
links.addAll(resource.getLinks());
jgen.writeStartObject();
final Map<String, Object> model = new LinkedHashMap<String, Object>();
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<Map<String, Object>> {
/**
* @param content
* @param links
*/
public MapResource(Map<String, Object> content, Iterable<Link> links) {
super(content, links);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.Resource#getContent()
*/
@Override
@JsonIgnore
public Map<String, Object> getContent() {
return super.getContent();
}
@JsonAnyGetter
public Map<String, Object> any() {
return getContent();
}
}
}

View File

@@ -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<Link> 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> 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<String> expectedRootLinkRels();
}

View File

@@ -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

View File

@@ -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());

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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())));
}
}