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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user