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:
@@ -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"
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
@@ -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<?>>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -12,5 +12,4 @@ public class RepositoryLinksResource extends Resources<Object> {
|
||||
public RepositoryLinksResource() {
|
||||
super(Collections.emptyList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user