diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriDomainClassConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java similarity index 88% rename from spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriDomainClassConverter.java rename to spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java index f430b51cb..e86cf16c5 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriDomainClassConverter.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/UriToEntityConverter.java @@ -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 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); } - } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java index 886e89568..24e3c1674 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java @@ -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()); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 03f8ae594..7c0223745 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -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()); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index f1b93ae4c..4a4ffd7ad 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -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> { + /** + * 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> { + 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, 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, 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 links = new ArrayList(); - // Start with ResourceProcessor-added links links.addAll(resource.getLinks()); - final Map model = new LinkedHashMap(); - final Collection fieldsToIgnore = new HashSet(); + // 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> 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> 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 resourceToRender = new Resource(obj, links); + provider.defaultSerializeValue(resourceToRender, jgen); } } - private static class MapResource extends Resource> { + /** + * {@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 content, Iterable 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 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 result = new ArrayList(); + 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 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 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 { + + 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); } } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java index 97ff8cf7c..f1a49c92c 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -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 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 uris = new ArrayList(links.length); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java index 856ffe51a..013f2388b 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java @@ -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)); + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java index 44caeb9ec..5832600aa 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -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 diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/util/TestUtils.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/util/TestUtils.java new file mode 100644 index 000000000..c8c0513c9 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/util/TestUtils.java @@ -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(); + } +}