DATAREST-248 - Significant overhaul in entity serialization and deserialization.

Refactored PersistentEntityJackson2Module to move a lot of the customization logic into Bean(De)SerializerModifiers. Those allow to modify the Jackson metadata for a given type programmatically so that we can register the appropriate (de)serializers for associations. 

On the serializing side of things this allows us to easily turn associations into links and simply delegate to object serialization as the delegation will simply ignore the association properties as they have been dropped using the modifier. This solves the advanced requirements of DATAREST-117 nicely as all non-association properties are serialized using standard Jackson means so that all customizations apply.

On the deserialization side, we now support URIs as values for association properties to be able to submit references for non-optional associations on creation.

Related issue: DATAREST-117.
This commit is contained in:
Oliver Gierke
2014-02-19 11:20:43 +01:00
parent cf41d4b692
commit 3e5914d84f
8 changed files with 458 additions and 156 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.util.Assert;
/**
@@ -33,7 +34,7 @@ import org.springframework.util.Assert;
* @author Jon Brisbin
* @author Oliver Gierke
*/
public class UriDomainClassConverter implements ConditionalGenericConverter {
public class UriToEntityConverter implements ConditionalGenericConverter {
private static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class);
@@ -41,14 +42,15 @@ public class UriDomainClassConverter implements ConditionalGenericConverter {
private final DomainClassConverter<?> domainClassConverter;
private final Set<ConvertiblePair> convertiblePairs;
private ResourceMappings mappings;
/**
* Creates a new {@link UriDomainClassConverter} using the given {@link Repositories} and {@link DomainClassConverter}
* .
* Creates a new {@link UriToEntityConverter} using the given {@link Repositories} and {@link DomainClassConverter}.
*
* @param repositories must not be {@literal null}.
* @param domainClassConverter must not be {@literal null}.
*/
public UriDomainClassConverter(Repositories repositories, DomainClassConverter<?> domainClassConverter) {
public UriToEntityConverter(Repositories repositories, DomainClassConverter<?> domainClassConverter) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(domainClassConverter, "DomainClassConverter must not be null!");
@@ -69,6 +71,8 @@ public class UriDomainClassConverter implements ConditionalGenericConverter {
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
mappings.exportsMappingFor(targetType.getType());
return URI.class.isAssignableFrom(sourceType.getType())
&& repositories.getPersistentEntity(targetType.getType()) != null;
}
@@ -106,5 +110,4 @@ public class UriDomainClassConverter implements ConditionalGenericConverter {
return domainClassConverter.convert(parts[parts.length - 1], STRING_TYPE, targetType);
}
}

View File

@@ -1,3 +1,18 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.core;
import org.springframework.beans.factory.annotation.Autowired;
@@ -7,7 +22,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriDomainClassConverter;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.domain.jpa.ConfiguredPersonRepository;
import org.springframework.data.rest.core.domain.jpa.JpaRepositoryConfig;
@@ -17,6 +31,7 @@ import org.springframework.format.support.DefaultFormattingConversionService;
/**
* @author Jon Brisbin
* @author Oliver Gierke
*/
@Configuration
@Import({ JpaRepositoryConfig.class })
@@ -56,7 +71,7 @@ public class RepositoryTestsConfig {
}
@Bean
public UriDomainClassConverter uriDomainClassConverter() {
return new UriDomainClassConverter(repositories(), domainClassConverter());
public UriToEntityConverter uriToEntityConverter() {
return new UriToEntityConverter(repositories(), domainClassConverter());
}
}

View File

@@ -36,7 +36,7 @@ import org.springframework.context.support.ReloadableResourceBundleMessageSource
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriDomainClassConverter;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.event.AnnotatedHandlerBeanPostProcessor;
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
@@ -51,8 +51,8 @@ import org.springframework.data.rest.webmvc.PersistentEntityResourceHandlerMetho
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping;
import org.springframework.data.rest.webmvc.RootResourceInformationHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.ResourceMetadataHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.RootResourceInformationHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver;
import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter;
import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper;
@@ -148,8 +148,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
}
@Bean
public UriDomainClassConverter uriDomainClassConverter() {
return new UriDomainClassConverter(repositories(), domainClassConverter());
public UriToEntityConverter uriToEntityConverter() {
return new UriToEntityConverter(repositories(), domainClassConverter());
}
/**
@@ -428,7 +428,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
*/
@Bean
public Module persistentEntityJackson2Module() {
return new PersistentEntityJackson2Module(resourceMappings(), defaultConversionService());
return new PersistentEntityJackson2Module(resourceMappings(), repositories(), config(), uriToEntityConverter());
}
/**

View File

@@ -16,25 +16,22 @@
package org.springframework.data.rest.webmvc.json;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.SimpleAssociationHandler;
import org.springframework.data.mapping.SimplePropertyHandler;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMappings;
@@ -45,17 +42,34 @@ import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerBuilder;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.type.CollectionLikeType;
/**
* Jackson 2 module to serialize and deserialize {@link PersistentEntityResource}s.
*
* @author Jon Brisbin
* @author Oliver Gierke
* @author Greg Turnquist
@@ -64,22 +78,30 @@ public class PersistentEntityJackson2Module extends SimpleModule {
private static final long serialVersionUID = -7289265674870906323L;
private static final Logger LOG = LoggerFactory.getLogger(PersistentEntityJackson2Module.class);
private static final TypeDescriptor URI_DESCRIPTOR = TypeDescriptor.valueOf(URI.class);
private final ResourceMappings mappings;
/**
* Creates a new {@link PersistentEntityJackson2Module} using the given {@link ResourceMappings}, {@link Repositories}
* , {@link RepositoryRestConfiguration} and {@link UriToEntityConverter}.
*
* @param mappings must not be {@literal null}.
* @param repositories must not be {@literal null}.
* @param config must not be {@literal null}.
* @param converter must not be {@literal null}.
*/
public PersistentEntityJackson2Module(ResourceMappings mappings, Repositories repositories,
RepositoryRestConfiguration config, UriToEntityConverter converter) {
@Autowired private Repositories repositories;
@Autowired private RepositoryRestConfiguration config;
super(new Version(2, 0, 0, null, "org.springframework.data.rest", "jackson-module"));
public PersistentEntityJackson2Module(ResourceMappings resourceMappings, ConversionService conversionService) {
Assert.notNull(mappings, "ResourceMappings must not be null!");
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(config, "RepositoryRestConfiguration must not be null!");
Assert.notNull(converter, "UriToEntityConverter must not be null!");
super(new Version(1, 1, 0, "BUILD-SNAPSHOT", "org.springframework.data.rest", "jackson-module"));
Assert.notNull(resourceMappings, "ResourceMappings must not be null!");
Assert.notNull(conversionService, "ConversionService must not be null!");
this.mappings = resourceMappings;
addSerializer(new ResourceSerializer());
addSerializer(new PersistentEntityResourceSerializer(mappings, config));
setSerializerModifier(new AssociationOmittingSerializerModifier(repositories, mappings, config));
setDeserializerModifier(new AssociationUriResolvingDeserializerModifier(repositories, converter, mappings));
}
public static boolean maybeAddAssociationLink(RepositoryLinkBuilder builder, ResourceMappings mappings,
@@ -104,11 +126,34 @@ public class PersistentEntityJackson2Module extends SimpleModule {
return false;
}
private class ResourceSerializer extends StdSerializer<PersistentEntityResource<?>> {
/**
* Custom {@link JsonSerializer} for {@link PersistentEntityResource}s to turn associations into {@link Link}s.
* Delegates to standard {@link Resource} serialization afterwards.
*
* @author Oliver Gierke
*/
private static class PersistentEntityResourceSerializer extends StdSerializer<PersistentEntityResource<?>> {
private final ResourceMappings mappings;
private final RepositoryRestConfiguration configuration;
/**
* Creates a new {@link PersistentEntityResourceSerializer} using the given {@link ResourceMappings} and
* {@link RepositoryRestConfiguration}.
*
* @param mappings must not be {@literal null}.
* @param configuration must not be {@literal null}.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private ResourceSerializer() {
private PersistentEntityResourceSerializer(ResourceMappings mappings, RepositoryRestConfiguration configuration) {
super((Class) PersistentEntityResource.class);
Assert.notNull(mappings, "ResourceMappings must not be null!");
Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!");
this.mappings = mappings;
this.configuration = configuration;
}
/*
@@ -124,117 +169,282 @@ public class PersistentEntityJackson2Module extends SimpleModule {
}
Object obj = resource.getContent();
PersistentEntity<?, ?> entity = resource.getPersistentEntity();
BeanWrapper<PersistentEntity<Object, ?>, Object> wrapper = BeanWrapper.create(obj, null);
Object entityId = wrapper.getProperty(entity.getIdProperty());
ResourceMetadata metadata = mappings.getMappingFor(entity.getType());
URI baseUri = configuration.getBaseUri();
final PersistentEntity<?, ?> entity = resource.getPersistentEntity();
final BeanWrapper<PersistentEntity<Object, ?>, Object> wrapper = BeanWrapper.create(obj, null);
final Object entityId = wrapper.getProperty(entity.getIdProperty());
final ResourceMetadata metadata = mappings.getMappingFor(entity.getType());
final RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, config.getBaseUri()).slash(entityId);
final RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, baseUri).slash(entityId);
final List<Link> links = new ArrayList<Link>();
// Start with ResourceProcessor-added links
links.addAll(resource.getLinks());
final Map<String, Object> model = new LinkedHashMap<String, Object>();
final Collection<String> fieldsToIgnore = new HashSet<String>();
// Add associations as links
entity.doWithAssociations(new SimpleAssociationHandler() {
JsonIgnoreProperties ann = entity.getType().getAnnotation(JsonIgnoreProperties.class);
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
*/
@Override
public void doWithAssociation(Association<? extends PersistentProperty<?>> association) {
if (ann != null) {
fieldsToIgnore.addAll(Arrays.asList(ann.value()));
}
PersistentProperty<?> property = association.getInverse();
try {
entity.doWithProperties(new SimplePropertyHandler() {
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.SimplePropertyHandler#doWithPersistentProperty(org.springframework.data.mapping.PersistentProperty)
*/
@Override
public void doWithPersistentProperty(PersistentProperty<?> property) {
boolean idAvailableAndShallNotBeExposed = property.isIdProperty()
&& !config.isIdExposedFor(entity.getType());
if (idAvailableAndShallNotBeExposed) {
return;
}
if (property.isAnnotationPresent(JsonIgnore.class)) {
return;
}
if (fieldsToIgnore.contains(property.getName())) {
return;
}
// Property is a normal or non-managed property.
model.put(property.getName(), wrapper.getProperty(property));
if (maybeAddAssociationLink(builder, mappings, property, links)) {
return;
}
});
}
});
// Add associations as links
entity.doWithAssociations(new SimpleAssociationHandler() {
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.SimpleAssociationHandler#doWithAssociation(org.springframework.data.mapping.Association)
*/
@Override
public void doWithAssociation(Association<? extends PersistentProperty<?>> association) {
PersistentProperty<?> property = association.getInverse();
if (maybeAddAssociationLink(builder, mappings, property, links)) {
return;
}
if (property.isAnnotationPresent(JsonIgnore.class)) {
return;
}
// Association Link was not added, probably because this isn't a managed type. Add value of property inline.
if (metadata.isExported(property)) {
model.put(property.getName(), wrapper.getProperty(property));
}
}
});
MapResource mapResource = new MapResource(model, links);
jgen.writeObject(mapResource);
} catch (IllegalStateException e) {
throw (IOException) e.getCause();
}
Resource<Object> resourceToRender = new Resource<Object>(obj, links);
provider.defaultSerializeValue(resourceToRender, jgen);
}
}
private static class MapResource extends Resource<Map<String, Object>> {
/**
* {@link BeanSerializerModifier} to drop the property descriptors for associations.
*
* @author Oliver Gierke
*/
private static class AssociationOmittingSerializerModifier extends BeanSerializerModifier {
private final Repositories repositories;
private final ResourceMappings mappings;
private final RepositoryRestConfiguration configuration;
/**
* @param content
* @param links
* Creates a new {@link AssociationOmittingSerializerModifier} for the given {@link Repositories},
* {@link ResourceMappings} and {@link RepositoryRestConfiguration}.
*
* @param repositories must not be {@literal null}.
* @param mappings must not be {@literal null}.
* @param configuration must not be {@literal null}.
*/
public MapResource(Map<String, Object> content, Iterable<Link> links) {
super(content, links);
private AssociationOmittingSerializerModifier(Repositories repositories, ResourceMappings mappings,
RepositoryRestConfiguration configuration) {
this.repositories = repositories;
this.mappings = mappings;
this.configuration = configuration;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.Resource#getContent()
* @see com.fasterxml.jackson.databind.ser.BeanSerializerModifier#updateBuilder(com.fasterxml.jackson.databind.SerializationConfig, com.fasterxml.jackson.databind.BeanDescription, com.fasterxml.jackson.databind.ser.BeanSerializerBuilder)
*/
@Override
@JsonIgnore
public Map<String, Object> getContent() {
return super.getContent();
public BeanSerializerBuilder updateBuilder(SerializationConfig config, BeanDescription beanDesc,
BeanSerializerBuilder builder) {
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(beanDesc.getBeanClass());
if (entity == null) {
return builder;
}
List<BeanPropertyWriter> result = new ArrayList<BeanPropertyWriter>();
ResourceMetadata resourceMetadata = mappings.getMappingFor(entity.getType());
for (BeanPropertyWriter writer : builder.getProperties()) {
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(writer.getName());
if (persistentProperty.isAssociation()) {
if (!resourceMetadata.isManagedResource(persistentProperty)) {
continue;
}
if (mappings.getMappingFor(persistentProperty.getActualType()).isExported()) {
continue;
}
ResourceMapping propertyMapping = resourceMetadata.getMappingFor(persistentProperty);
if (!propertyMapping.isExported()) {
continue;
}
}
if (persistentProperty.isIdProperty() && !configuration.isIdExposedFor(entity.getType())) {
continue;
}
result.add(writer);
}
builder.setProperties(result);
return builder;
}
}
/**
* A {@link BeanDeserializerModifier} that registers a custom {@link UriStringDeserializer} for association properties
* of {@link PersistentEntity}s. This allows to submit URIs for those properties in request payloads, so that
* non-optional associations can be populated on resource creation.
*
* @author Oliver Gierke
*/
private static class AssociationUriResolvingDeserializerModifier extends BeanDeserializerModifier {
private final UriToEntityConverter converter;
private final Repositories repositories;
private final ResourceMappings mappings;
/**
* Creates a new {@link AssociationUriResolvingDeserializerModifier} using the given {@link Repositories},
* {@link UriToEntityConverter} and {@link ResourceMappings}.
*
* @param repositories must not be {@literal null}.
* @param converter must not be {@literal null}.
* @param mappings must not be {@literal null}.
*/
public AssociationUriResolvingDeserializerModifier(Repositories repositories, UriToEntityConverter converter,
ResourceMappings mappings) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(converter, "UriToEntityConverter must not be null!");
Assert.notNull(mappings, "ResourceMappings must not be null!");
this.repositories = repositories;
this.converter = converter;
this.mappings = mappings;
}
@JsonAnyGetter
public Map<String, Object> any() {
return getContent();
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.BeanDeserializerModifier#updateBuilder(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.BeanDescription, com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder)
*/
@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc,
BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> properties = builder.getProperties();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(beanDesc.getBeanClass());
ResourceMetadata metadata = mappings.getMappingFor(beanDesc.getBeanClass());
if (entity == null) {
return builder;
}
while (properties.hasNext()) {
SettableBeanProperty property = properties.next();
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(property.getName());
ResourceMapping propertyMapping = metadata.getMappingFor(persistentProperty);
if (!persistentProperty.isAssociation() || !propertyMapping.isExported()) {
continue;
}
UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(persistentProperty, converter);
if (persistentProperty.isCollectionLike()) {
CollectionLikeType collectionType = config.getTypeFactory().constructCollectionLikeType(
persistentProperty.getType(), persistentProperty.getActualType());
CollectionValueInstantiator instantiator = new CollectionValueInstantiator(persistentProperty);
CollectionDeserializer collectionDeserializer = new CollectionDeserializer(collectionType,
uriStringDeserializer, null, instantiator);
builder.addOrReplaceProperty(property.withValueDeserializer(collectionDeserializer), false);
} else {
builder.addOrReplaceProperty(property.withValueDeserializer(uriStringDeserializer), false);
}
}
return builder;
}
}
/**
* Custom {@link JsonDeserializer} to interpret {@link String} values as URIs and resolve them using a
* {@link UriToEntityConverter}.
*
* @author Oliver Gierke
*/
private static class UriStringDeserializer extends StdDeserializer<Object> {
private static final long serialVersionUID = -2175900204153350125L;
private final PersistentProperty<?> property;
private final UriToEntityConverter converter;
/**
* Creates a new {@link UriStringDeserializer} for the given {@link PersistentProperty} using the given
* {@link UriToEntityConverter}.
*
* @param property must not be {@literal null}.
* @param converter must not be {@literal null}.
*/
public UriStringDeserializer(PersistentProperty<?> property, UriToEntityConverter converter) {
super(property.getActualType());
this.property = property;
this.converter = converter;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String uriString = jp.getValueAsString();
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(property.getActualType());
return converter.convert(URI.create(uriString), URI_DESCRIPTOR, typeDescriptor);
}
}
/**
* {@link ValueInstantiator} to create collection or map instances based on the type of the configured
* {@link PersistentProperty}.
*
* @author Oliver Gierke
*/
private static class CollectionValueInstantiator extends ValueInstantiator {
private final PersistentProperty<?> property;
/**
* Creates a new {@link CollectionValueInstantiator} for the given {@link PersistentProperty}.
*
* @param property must not be {@literal null} and must be a collection.
*/
public CollectionValueInstantiator(PersistentProperty<?> property) {
Assert.notNull(property, "Property must not be null!");
Assert.isTrue(property.isCollectionLike() || property.isMap(), "Property must be a collection or map property!");
this.property = property;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.ValueInstantiator#getValueTypeDesc()
*/
@Override
public String getValueTypeDesc() {
return property.getType().getName();
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.ValueInstantiator#createUsingDefault(com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public Object createUsingDefault(DeserializationContext ctxt) throws IOException, JsonProcessingException {
Class<?> collectionOrMapType = property.getType();
return property.isMap() ? CollectionFactory.createMap(collectionOrMapType, 0) : CollectionFactory
.createCollection(collectionOrMapType, 0);
}
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc.jpa;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.rest.webmvc.util.TestUtils.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@@ -25,12 +26,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests;
import org.springframework.hateoas.Link;
@@ -90,7 +89,7 @@ public class JpaWebTests extends AbstractWebIntegrationTests {
*/
@Override
protected Map<String, String> getPayloadToPost() throws Exception {
return Collections.singletonMap("people", readFile("person.json"));
return Collections.singletonMap("people", readFileFromClasspath("person.json"));
}
/*
@@ -172,7 +171,7 @@ public class JpaWebTests extends AbstractWebIntegrationTests {
mvc.perform(//
put("/orders/{id}", 4711).//
content(readFile("order.json")).contentType(MediaType.APPLICATION_JSON)//
content(readFileFromClasspath("order.json")).contentType(MediaType.APPLICATION_JSON)//
).andExpect(status().isCreated());
}
@@ -447,26 +446,6 @@ public class JpaWebTests extends AbstractWebIntegrationTests {
assertThat(persons, hasItems(siblingNames));
}
private static String readFile(String name) throws Exception {
ClassPathResource file = new ClassPathResource(name, JpaWebTests.class);
StringBuilder builder = new StringBuilder();
Scanner scanner = new Scanner(file.getFile(), "UTF-8");
try {
while (scanner.hasNextLine()) {
builder.append(scanner.nextLine());
}
} finally {
scanner.close();
}
return builder.toString();
}
private static String toUriList(Link... links) {
List<String> uris = new ArrayList<String>(links.length);

View File

@@ -29,8 +29,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.jpa.Order;
import org.springframework.data.rest.webmvc.jpa.Person;
import org.springframework.data.rest.webmvc.jpa.PersonRepository;
import org.springframework.data.rest.webmvc.util.TestUtils;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.hal.HalLinkDiscoverer;
@@ -42,8 +44,11 @@ import org.springframework.web.util.UriTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Integration tests for entity (de)serialization.
*
* @author Jon Brisbin
* @author Greg Turnquist
* @author Oliver Gierke
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RepositoryTestsConfig.class)
@@ -111,4 +116,46 @@ public class PersistentEntitySerializationTests {
Link siblingLink = linkDiscoverer.findLinkWithRel("siblings", s);
assertThat(siblingLink.getHref(), endsWith(new UriTemplate("/{id}/siblings").expand(person.getId()).toString()));
}
/**
* @see DATAREST-248
*/
@Test
public void deserializesPersonWithLinkToOtherPersonCorrectly() throws Exception {
Person father = people.save(new Person("John", "Doe"));
String child = String.format("{ \"firstName\" : \"Bilbo\", \"father\" : \"/persons/%s\"}", father.getId());
Person result = mapper.readValue(child, Person.class);
assertThat(result.getFather(), is(father));
}
/**
* @see DATAREST-248
*/
@Test
public void deserializesPersonWithLinkToOtherPersonsCorrectly() throws Exception {
Person firstSibling = people.save(new Person("John", "Doe"));
Person secondSibling = people.save(new Person("Dave", "Doe"));
String child = String.format("{ \"firstName\" : \"Bilbo\", \"siblings\" : [\"/persons/%s\", \"/persons/%s\"]}",
firstSibling.getId(), secondSibling.getId());
Person result = mapper.readValue(child, Person.class);
assertThat(result.getSiblings(), hasItems(firstSibling, secondSibling));
}
/**
* @see DATAREST-248
*/
@Test
public void deserializesEmbeddedAssociationsCorrectly() throws Exception {
String content = TestUtils.readFileFromClasspath("order.json");
Order order = mapper.readValue(content, Order.class);
assertThat(order.getLineItems(), hasSize(2));
}
}

View File

@@ -24,7 +24,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriDomainClassConverter;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
@@ -42,6 +42,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Jon Brisbin
* @author Greg Trunquist
* @author Oliver Gierke
*/
@Configuration
@Import({ JpaRepositoryConfig.class })
@@ -85,14 +86,14 @@ public class RepositoryTestsConfig {
}
@Bean
public UriDomainClassConverter uriDomainClassConverter() {
return new UriDomainClassConverter(repositories(), domainClassConverter());
public UriToEntityConverter uriToEntityConverter() {
return new UriToEntityConverter(repositories(), domainClassConverter());
}
@Bean
public Module persistentEntityModule() {
return new PersistentEntityJackson2Module(new ResourceMappings(config(), repositories()),
defaultConversionService());
return new PersistentEntityJackson2Module(new ResourceMappings(config(), repositories()), repositories(), config(),
uriToEntityConverter());
}
@Bean

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc.util;
import java.util.Scanner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.rest.webmvc.jpa.JpaWebTests;
/**
* @author Oliver Gierke
*/
public class TestUtils {
public static String readFileFromClasspath(String name) throws Exception {
ClassPathResource file = new ClassPathResource(name, JpaWebTests.class);
StringBuilder builder = new StringBuilder();
Scanner scanner = new Scanner(file.getFile(), "UTF-8");
try {
while (scanner.hasNextLine()) {
builder.append(scanner.nextLine());
}
} finally {
scanner.close();
}
return builder.toString();
}
}