diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Link.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Link.java index ba828fc33..8d9127dcd 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Link.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Link.java @@ -9,7 +9,6 @@ import org.codehaus.jackson.annotate.JsonTypeInfo; * * @author Jon Brisbin */ -@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, defaultImpl = ResourceLink.class) public interface Link { /** diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceLink.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceLink.java index 804676705..e1549a945 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceLink.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/ResourceLink.java @@ -2,8 +2,8 @@ package org.springframework.data.rest.core; import java.net.URI; -import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonProperty; +import org.springframework.util.Assert; /** * Implementation of {@link Link}. @@ -33,6 +33,25 @@ public class ResourceLink implements Link, Comparable { return href; } + public String getRel() { + return rel(); + } + + public ResourceLink setRel(String rel) { + this.rel = rel; + return this; + } + + public URI getHref() { + return href(); + } + + public ResourceLink setHref(URI href) { + Assert.notNull(href, "href URI cannot be null."); + this.href = href; + return this; + } + @Override public int compareTo(Link link) { if(null == rel || null == link.rel()) { return -1; diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java deleted file mode 100644 index 03a00822d..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.springframework.data.rest.core.util; - -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.deser.std.StdDeserializer; -import org.springframework.core.convert.ConversionService; -import org.springframework.util.ClassUtils; - -/** - * A "fluent" bean is one that does not use the JavaBean conventions of "setProperty" and "getProperty" but instead - * uses just "property" with 0 or 1 arguments to distinguish between getter (0 arg) and setter (1 arg). - * - * @author Jon Brisbin - */ -public class FluentBeanDeserializer extends StdDeserializer { - - private ConversionService conversionService; - private FluentBeanUtils.Metadata beanMeta; - - @SuppressWarnings({"unchecked"}) - public FluentBeanDeserializer(final Class valueClass, ConversionService conversionService) { - super(valueClass); - this.conversionService = conversionService; - this.beanMeta = FluentBeanUtils.metadata(valueClass); - - if(!FluentBeanUtils.isFluentBean(valueClass)) { - throw new IllegalArgumentException("Class of type " + valueClass + " is not a FluentBean"); - } - } - - @Override - public Object deserialize(JsonParser jp, - DeserializationContext ctxt) throws IOException { - if(jp.getCurrentToken() != JsonToken.START_OBJECT) { - throw ctxt.mappingException(_valueClass); - } - - Object bean; - try { - bean = _valueClass.newInstance(); - } catch(InstantiationException e) { - throw new IllegalStateException(e); - } catch(IllegalAccessException e) { - throw new IllegalStateException(e); - } - - while(jp.nextToken() != JsonToken.END_OBJECT) { - String name = jp.getCurrentName(); - Method setter = beanMeta.setters().get(name); - - Object obj; - if(null != setter) { - Class targetType = setter.getParameterTypes()[0]; - if(ClassUtils.isAssignable(targetType, Long.class)) { - obj = jp.nextLongValue(-1); - } else if(ClassUtils.isAssignable(targetType, Integer.class)) { - obj = jp.nextIntValue(-1); - } else if(ClassUtils.isAssignable(targetType, Boolean.class)) { - obj = jp.nextBooleanValue(); - } else { - obj = jp.nextTextValue(); - } - - if(null != obj) { - if(!ClassUtils.isAssignable(obj.getClass(), targetType)) { - obj = conversionService.convert(obj, targetType); - } - - try { - setter.invoke(bean, obj); - } catch(IllegalAccessException e) { - throw new IllegalStateException(e); - } catch(InvocationTargetException e) { - throw new IllegalStateException(e); - } - } - } - - } - - return bean; - } - -} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java deleted file mode 100644 index 463aba1dd..000000000 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.springframework.data.rest.core.util; - -import java.io.IOException; -import java.util.Collection; -import java.util.Map; - -import org.codehaus.jackson.JsonGenerationException; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.map.SerializerProvider; -import org.codehaus.jackson.map.ser.std.SerializerBase; -import org.springframework.util.ClassUtils; - -/** - * A "fluent" bean is one that does not use the JavaBean conventions of "setProperty" and "getProperty" but instead - * uses just "property" with 0 or 1 arguments to distinguish between getter (0 arg) and setter (1 arg). - * - * @author Jon Brisbin - */ -public class FluentBeanSerializer - extends SerializerBase { - - @SuppressWarnings({"unchecked", "rawtypes"}) - public FluentBeanSerializer(final Class t) { - super(t); - - if(!FluentBeanUtils.isFluentBean(t)) { - throw new IllegalArgumentException("Class of type " + t + " is not a FluentBean"); - } - } - - @SuppressWarnings({"unchecked"}) - @Override - public void serialize(final Object value, - final JsonGenerator jgen, - final SerializerProvider provider) - throws IOException, - JsonGenerationException { - if(null == value) { - provider.defaultSerializeNull(jgen); - } else { - Class type = value.getClass(); - if(ClassUtils.isAssignable(type, Collection.class)) { - jgen.writeStartArray(); - for(Object o : (Collection)value) { - write(o, jgen, provider); - } - jgen.writeEndArray(); - } else if(ClassUtils.isAssignable(type, Map.class)) { - jgen.writeStartObject(); - for(Map.Entry entry : ((Map)value).entrySet()) { - jgen.writeFieldName(entry.getKey()); - write(entry.getValue(), jgen, provider); - } - jgen.writeEndObject(); - } else { - write(value, jgen, provider); - } - } - } - - private void write(final Object value, - final JsonGenerator jgen, - final SerializerProvider provider) - throws IOException { - Class type = value.getClass(); - if(ClassUtils.isAssignable(type, _handledType)) { - jgen.writeStartObject(); - for(String fname : FluentBeanUtils.metadata(type).fieldNames()) { - jgen.writeFieldName(fname); - write(FluentBeanUtils.get(fname, value), jgen, provider); - } - jgen.writeEndObject(); - } else { - jgen.writeObject(value); - } - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java index 584fdb03f..086ae9dff 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java @@ -25,6 +25,13 @@ public interface AttributeMetadata { */ Class type(); + /** + * The type of this map's key, if it's map-like. + * + * @return + */ + Class keyType(); + /** * The element type of this attribute, if this attribute is a "plural"-like attribute (a Collection, Map, etc...). * diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResourceSet.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResourceSet.java index 4f0b7ccfd..d392cef29 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResourceSet.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PageableResourceSet.java @@ -8,18 +8,8 @@ import org.springframework.data.rest.core.ResourceSet; */ public class PageableResourceSet extends ResourceSet { - protected long resourceCount = 0; @JsonProperty("page") - protected PagingMetadata paging = new PagingMetadata(-1, 0); - - public long getResourceCount() { - return resourceCount; - } - - public PageableResourceSet setResourceCount(long resourceCount) { - this.resourceCount = resourceCount; - return this; - } + protected PagingMetadata paging = new PagingMetadata(-1, 20, 0, 0); public PagingMetadata getPaging() { return paging; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java index dee278ea7..8678cb3c1 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingMetadata.java @@ -5,29 +5,54 @@ package org.springframework.data.rest.repository; */ public class PagingMetadata { - private int current = 0; - private int total = 0; + private int number = 0; + private int size = 0; + private int totalPages = 0; + private long totalElements = 0; - public PagingMetadata(int current, int total) { - this.current = current; - this.total = total; + public PagingMetadata(int number, + int size, + int totalPages, + long totalElements) { + this.number = number; + this.size = size; + this.totalPages = totalPages; + this.totalElements = totalElements; } - public int getCurrent() { - return current; + public int getNumber() { + return number; } - public PagingMetadata setCurrent(int current) { - this.current = current; + public PagingMetadata setNumber(int number) { + this.number = number; return this; } - public int getTotal() { - return total; + public int getSize() { + return size; } - public PagingMetadata setTotal(int total) { - this.total = total; + public PagingMetadata setSize(int size) { + this.size = size; + return this; + } + + public int getTotalPages() { + return totalPages; + } + + public PagingMetadata setTotalPages(int totalPages) { + this.totalPages = totalPages; + return this; + } + + public long getTotalElements() { + return totalElements; + } + + public PagingMetadata setTotalElements(long totalElements) { + this.totalElements = totalElements; return this; } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java index fefb149e1..1af03a408 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java @@ -13,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired; */ public abstract class RepositoryExporterSupport> { - @Autowired(required = false) protected List repositoryExporters = Collections.emptyList(); /** @@ -31,6 +30,7 @@ public abstract class RepositoryExporterSupport repositoryExporters) { this.repositoryExporters = repositoryExporters; } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadataResolver.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadataResolver.java deleted file mode 100644 index 92e21b719..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMetadataResolver.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.springframework.data.rest.repository; - -import static org.springframework.data.rest.core.util.UriUtils.*; - -import java.net.URI; -import java.util.Collections; -import java.util.List; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.data.rest.core.Resolver; -import org.springframework.util.Assert; - -/** - * @author Jon Brisbin - */ -public class RepositoryMetadataResolver, E extends EntityMetadata> - implements Resolver, - ApplicationContextAware { - - private ApplicationContext applicationContext; - @Autowired(required = false) - private List repositoryExporters = Collections.emptyList(); - - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - public List getRepositoryExporters() { - return repositoryExporters; - } - - public RepositoryMetadataResolver setRepositoryExporters(List repositoryExporters) { - Assert.notNull(repositoryExporters, "List of RepositoryExporters cannot be null!"); - this.repositoryExporters = repositoryExporters; - return this; - } - - @SuppressWarnings({"unchecked"}) - @Override public M resolve(URI baseUri, URI uri) { - URI tail; - if(null == (tail = tail(baseUri, uri))) { - return null; - } - - String path = tail.getPath(); - for(RepositoryExporter exporter : repositoryExporters) { - RepositoryMetadata repoMeta; - if(null != (repoMeta = exporter.repositoryMetadataFor(path))) { - return (M)repoMeta; - } - } - - return null; - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriToDomainObjectResolver.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriToDomainObjectResolver.java new file mode 100644 index 000000000..750cf43f5 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriToDomainObjectResolver.java @@ -0,0 +1,69 @@ +package org.springframework.data.rest.repository; + +import java.io.Serializable; +import java.net.URI; +import java.util.Stack; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.core.Resolver; +import org.springframework.data.rest.core.util.UriUtils; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.util.ClassUtils; + +/** + * @author Jon Brisbin + */ +public class UriToDomainObjectResolver + extends RepositoryExporterSupport + implements Resolver { + + @Autowired(required = false) + private ConversionService conversionService = new DefaultFormattingConversionService(); + + public ConversionService getConversionService() { + return conversionService; + } + + public UriToDomainObjectResolver setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + return this; + } + + @SuppressWarnings({"unchecked"}) + @Override public Object resolve(URI baseUri, URI uri) { + URI relativeUri = baseUri.relativize(uri); + Stack uris = UriUtils.explode(baseUri, relativeUri); + + if(uris.size() < 1) { + return null; + } + + String repoName = UriUtils.path(uris.get(0)); + String sId = UriUtils.path(uris.get(1)); + + RepositoryMetadata repoMeta = repositoryMetadataFor(repoName); + + CrudRepository repo; + if(null == (repo = repoMeta.repository())) { + return null; + } + + EntityMetadata entityMeta; + if(null == (entityMeta = repoMeta.entityMetadata())) { + return null; + } + + Class idType = (Class)entityMeta.idAttribute().type(); + Serializable serId; + if(ClassUtils.isAssignable(idType, String.class)) { + serId = sId; + } else { + serId = conversionService.convert(sId, idType); + } + + return repo.findOne(serId); + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java index 25ac5a0e8..109bf3861 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.Set; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.MapAttribute; import javax.persistence.metamodel.PluralAttribute; import org.springframework.beans.BeanUtils; @@ -58,6 +59,12 @@ public class JpaAttributeMetadata implements AttributeMetadata { return type; } + @Override public Class keyType() { + return (attribute instanceof MapAttribute + ? ((MapAttribute)attribute).getKeyJavaType() + : null); + } + @Override public Class elementType() { return (attribute instanceof PluralAttribute ? ((PluralAttribute)attribute).getElementType().getJavaType() diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/JacksonUtil.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/JacksonUtil.java index 49bb589e2..7336aae49 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/JacksonUtil.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/JacksonUtil.java @@ -6,10 +6,6 @@ import java.util.Arrays; import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.map.ser.CustomSerializerFactory; -import org.springframework.data.rest.core.Link; -import org.springframework.data.rest.core.ResourceLink; -import org.springframework.data.rest.core.util.FluentBeanSerializer; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageNotWritableException; @@ -27,25 +23,34 @@ public abstract class JacksonUtil { } public static MappingJacksonHttpMessageConverter createJacksonHttpMessageConverter(final ObjectMapper objectMapper) { - // We need a custom serializer for handling beans that don't conform to the JavaBeans standard 'get' and 'set' - CustomSerializerFactory customSerializerFactory = new CustomSerializerFactory(); - customSerializerFactory.addSpecificMapping(ResourceLink.class, new FluentBeanSerializer(ResourceLink.class)); - objectMapper.setSerializerFactory(customSerializerFactory); // We want to support all our custom types of JSON and also the catch-all MappingJacksonHttpMessageConverter jsonConverter = new MappingJacksonHttpMessageConverter() { { setSupportedMediaTypes(Arrays.asList( MediaType.APPLICATION_JSON, MediaTypes.COMPACT_JSON, - MediaTypes.VERBOSE_JSON, - MediaType.ALL + MediaTypes.VERBOSE_JSON )); } + @Override public boolean canRead(Class clazz, MediaType mediaType) { + if(!canRead(mediaType)) { + return false; + } + return true; + } + + @Override public boolean canWrite(Class clazz, MediaType mediaType) { + if(!canWrite(mediaType)) { + return false; + } + return true; + } + @Override - protected void writeInternal(Object object, HttpOutputMessage outputMessage) - throws IOException, - HttpMessageNotWritableException { + protected void writeInternal(Object object, + HttpOutputMessage outputMessage) throws IOException, + HttpMessageNotWritableException { JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); // Believe it or not, this is the only way to get pretty-printing from Jackson in this configuration JsonGenerator jsonGenerator = objectMapper diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java index 224d66653..af541a63c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java @@ -14,8 +14,7 @@ import org.springframework.web.util.UriComponentsBuilder; * * @author Jon Brisbin */ -public class PagingAndSorting - implements Pageable { +public class PagingAndSorting implements Pageable { private final RepositoryRestConfiguration config; private final PageRequest pageRequest; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryAwareMappingHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryAwareMappingHttpMessageConverter.java new file mode 100644 index 000000000..6c3c2e23d --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryAwareMappingHttpMessageConverter.java @@ -0,0 +1,431 @@ +package org.springframework.data.rest.webmvc; + +import java.io.IOException; +import java.io.Serializable; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.codehaus.jackson.JsonEncoding; +import org.codehaus.jackson.JsonGenerationException; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.JsonToken; +import org.codehaus.jackson.Version; +import org.codehaus.jackson.map.DeserializationContext; +import org.codehaus.jackson.map.KeyDeserializer; +import org.codehaus.jackson.map.Module; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializerProvider; +import org.codehaus.jackson.map.deser.std.StdDeserializer; +import org.codehaus.jackson.map.module.SimpleAbstractTypeResolver; +import org.codehaus.jackson.map.module.SimpleDeserializers; +import org.codehaus.jackson.map.module.SimpleKeyDeserializers; +import org.codehaus.jackson.map.module.SimpleModule; +import org.codehaus.jackson.map.module.SimpleSerializers; +import org.codehaus.jackson.map.ser.std.SerializerBase; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.rest.core.Link; +import org.springframework.data.rest.core.ResourceLink; +import org.springframework.data.rest.repository.AttributeMetadata; +import org.springframework.data.rest.repository.RepositoryExporter; +import org.springframework.data.rest.repository.RepositoryMetadata; +import org.springframework.data.rest.repository.UriToDomainObjectResolver; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * @author Jon Brisbin + */ +public class RepositoryAwareMappingHttpMessageConverter + extends MappingJacksonHttpMessageConverter + implements InitializingBean { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Autowired(required = false) + protected ConversionService conversionService = new DefaultFormattingConversionService(); + @Autowired(required = false) + protected List repositoryExporters = Collections.emptyList(); + @Autowired(required = false) + protected List modules = Collections.emptyList(); + @Autowired + protected UriToDomainObjectResolver domainObjectResolver; + + public RepositoryAwareMappingHttpMessageConverter() { + setSupportedMediaTypes(Arrays.asList( + MediaType.APPLICATION_JSON, + MediaTypes.COMPACT_JSON, + MediaTypes.VERBOSE_JSON + )); + + mapper.registerModule(new RepositoryAwareModule()); + + setObjectMapper(mapper); + } + + @Override public void afterPropertiesSet() throws Exception { + for(Module m : modules) { + mapper.registerModule(m); + } + } + + public ConversionService getConversionService() { + return conversionService; + } + + public RepositoryAwareMappingHttpMessageConverter setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + return this; + } + + public List getRepositoryExporters() { + return repositoryExporters; + } + + @Autowired(required = false) + @SuppressWarnings({"unchecked"}) + public RepositoryAwareMappingHttpMessageConverter setRepositoryExporters(List repositoryExporters) { + this.repositoryExporters = repositoryExporters; + + for(RepositoryExporter repoExp : repositoryExporters) { + for(String repoName : new ArrayList(repoExp.repositoryNames())) { + RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName); + Class domainType = repoMeta.entityMetadata().type(); + + } + } + return this; + } + + public List getModules() { + return modules; + } + + public RepositoryAwareMappingHttpMessageConverter setModules(List modules) { + this.modules = modules; + return this; + } + + public UriToDomainObjectResolver getDomainObjectResolver() { + return domainObjectResolver; + } + + public RepositoryAwareMappingHttpMessageConverter setDomainObjectResolver(UriToDomainObjectResolver domainObjectResolver) { + this.domainObjectResolver = domainObjectResolver; + return this; + } + + @Override public boolean canWrite(Class clazz, MediaType mediaType) { + if(!canWrite(mediaType)) { + return false; + } + return supports(clazz); + } + + @Override public boolean canRead(Class clazz, MediaType mediaType) { + if(!canRead(mediaType)) { + return false; + } + return supports(clazz); + } + + @SuppressWarnings({"unchecked"}) + @Override protected boolean supports(Class clazz) { + for(RepositoryExporter repoExp : repositoryExporters) { + for(String repoName : new ArrayList(repoExp.repositoryNames())) { + RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName); + Class domainType = repoMeta.entityMetadata().type(); + if(domainType.isAssignableFrom(clazz)) { + return true; + } + } + } + return false; + } + + @Override protected void writeInternal(Object object, + HttpOutputMessage outputMessage) throws IOException, + HttpMessageNotWritableException { + JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); + // Believe it or not, this is the only way to get pretty-printing from Jackson in this configuration + JsonGenerator jsonGenerator = mapper + .getJsonFactory() + .createJsonGenerator(outputMessage.getBody(), encoding) + .useDefaultPrettyPrinter(); + try { + mapper.writeValue(jsonGenerator, object); + } catch(IOException ex) { + throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); + } + } + + @SuppressWarnings({"unchecked"}) + private RepositoryMetadata repositoryMetadataFor(Class domainType) { + for(RepositoryExporter repoExp : repositoryExporters) { + if(repoExp.hasRepositoryFor(domainType)) { + return repoExp.repositoryMetadataFor(domainType); + } + } + return null; + } + + private URI buildUri(URI baseUri, String... pathSegments) { + return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri(); + } + + private class RepositoryAwareModule extends SimpleModule { + private RepositoryAwareModule() { + super("RepositoryAwareModule", new Version(1, 0, 0, "SNAPSHOT")); + } + + @SuppressWarnings({"unchecked"}) + @Override public void setupModule(SetupContext context) { + context.addAbstractTypeResolver( + new SimpleAbstractTypeResolver() + .addMapping(Link.class, ResourceLink.class) + ); + SimpleSerializers sers = new SimpleSerializers(); + SimpleDeserializers dsers = new SimpleDeserializers(); + SimpleSerializers keySers = new SimpleSerializers(); + SimpleKeyDeserializers keyDsers = new SimpleKeyDeserializers(); + + for(RepositoryExporter repoExp : repositoryExporters) { + for(String repoName : new ArrayList(repoExp.repositoryNames())) { + RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName); + Class domainType = repoMeta.entityMetadata().type(); + + sers.addSerializer(domainType, new DomainObjectToLinkSerializer(domainType, repoMeta)); + keySers.addSerializer(domainType, new DomainObjectToStringKeySerializer(domainType, repoMeta)); + + dsers.addDeserializer(domainType, new LinkToDomainObjectDeserializer(domainType, repoMeta)); + keyDsers.addDeserializer(domainType, new KeyToDomainObjectDeserializer()); + } + } + + context.addSerializers(sers); + context.addKeySerializers(keySers); + context.addDeserializers(dsers); + context.addKeyDeserializers(keyDsers); + } + } + + private class DomainObjectToLinkSerializer extends SerializerBase { + + protected final RepositoryMetadata repoMeta; + protected final AttributeMetadata idAttr; + + private DomainObjectToLinkSerializer(Class t, RepositoryMetadata repoMeta) { + super(t); + this.repoMeta = repoMeta; + if(null != repoMeta) { + idAttr = repoMeta.entityMetadata().idAttribute(); + } else { + idAttr = null; + } + } + + @Override public void serialize(Object value, + JsonGenerator jgen, + SerializerProvider provider) throws IOException, + JsonGenerationException { + if(null == value) { + provider.defaultSerializeNull(jgen); + return; + } + if(null == repoMeta) { + provider.defaultSerializeValue(value, jgen); + return; + } + + Serializable serId = (Serializable)idAttr.get(value); + String sId = conversionService.convert(serId, String.class); + if(null == sId) { + sId = serId.toString(); + } + + String rel = repoMeta.rel() + "." + repoMeta.domainType().getSimpleName(); + URI href = buildUri(RepositoryRestController.BASE_URI.get(), repoMeta.name(), sId); + + jgen.writeObject(new ResourceLink(rel, href)); + } + + } + + private class DomainObjectToStringKeySerializer extends DomainObjectToLinkSerializer { + + private DomainObjectToStringKeySerializer(Class t, RepositoryMetadata repoMeta) { + super(t, repoMeta); + } + + @Override public void serialize(Object value, + JsonGenerator jgen, + SerializerProvider provider) throws IOException, + JsonGenerationException { + if(null == value) { + provider.defaultSerializeNull(jgen); + return; + } + if(null == repoMeta) { + provider.defaultSerializeValue(value, jgen); + return; + } + + Serializable serId = (Serializable)idAttr.get(value); + String sId = conversionService.convert(serId, String.class); + if(null == sId) { + sId = serId.toString(); + } + + URI href = buildUri(RepositoryRestController.BASE_URI.get(), repoMeta.name(), sId); + + jgen.writeString("@" + href.toString()); + } + + } + + private class LinkToDomainObjectDeserializer extends StdDeserializer { + + protected final RepositoryMetadata repoMeta; + + private LinkToDomainObjectDeserializer(Class vc, RepositoryMetadata repoMeta) { + super(vc); + this.repoMeta = repoMeta; + } + + @SuppressWarnings({"unchecked"}) + @Override public Object deserialize(JsonParser jp, + DeserializationContext ctxt) throws IOException, + JsonProcessingException { + Object entity; + try { + entity = getValueClass().newInstance(); + } catch(InstantiationException e) { + throw ctxt.instantiationException(getValueClass(), e); + } catch(IllegalAccessException e) { + throw ctxt.instantiationException(getValueClass(), e); + } + + for(JsonToken tok = jp.getCurrentToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) { + String name = jp.getCurrentName(); + switch(tok) { + case FIELD_NAME: { + if(name.startsWith("@http")) { + entity = domainObjectResolver.resolve( + RepositoryRestController.BASE_URI.get(), + URI.create(name.substring(1)) + ); + } else if("href".equals(name)) { + entity = domainObjectResolver.resolve( + RepositoryRestController.BASE_URI.get(), + URI.create(jp.nextTextValue()) + ); + } else if("rel".equals(name)) { + // rel is currently ignored + } else { + // Read the attribute metadata + AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(name); + if(null == attrMeta) { + continue; + } + // Try and read the value of this attribute. + // The method of doing that varies based on the type of the property. + Object val; + if(attrMeta.isCollectionLike()) { + Collection c = attrMeta.asCollection(entity); + if(null == c) { + c = new ArrayList(); + } + + if(jp.getCurrentToken() == JsonToken.START_ARRAY) { + while((tok = jp.nextToken()) != JsonToken.END_ARRAY) { + Object cval = jp.readValueAs(attrMeta.elementType()); + c.add(cval); + } + } + + val = c; + } else if(attrMeta.isSetLike()) { + Set s = attrMeta.asSet(entity); + if(null == s) { + s = new HashSet(); + } + + if(jp.getCurrentToken() == JsonToken.START_ARRAY) { + while((tok = jp.nextToken()) != JsonToken.END_ARRAY) { + Object sval = jp.readValueAs(attrMeta.elementType()); + s.add(sval); + } + } + + val = s; + } else if(attrMeta.isMapLike()) { + Map m = attrMeta.asMap(entity); + if(null == m) { + m = new HashMap(); + } + + if(jp.getCurrentToken() == JsonToken.START_OBJECT) { + while((tok = jp.nextToken()) != JsonToken.END_OBJECT) { + name = jp.getCurrentName(); + Object mkey = ( + name.startsWith("@http") + ? domainObjectResolver.resolve( + RepositoryRestController.BASE_URI.get(), + URI.create(name.substring(1)) + ) + : name + ); + tok = jp.nextToken(); + Object mval = jp.readValueAs(attrMeta.elementType()); + + m.put(mkey, mval); + } + } + + val = m; + } else { + val = jp.readValueAs(attrMeta.type()); + } + attrMeta.set(val, entity); + } + break; + } + } + } + + return entity; + } + + } + + private class KeyToDomainObjectDeserializer extends KeyDeserializer { + @Override public Object deserializeKey(String key, + DeserializationContext ctxt) throws IOException, + JsonProcessingException { + if(key.startsWith("@http")) { + return domainObjectResolver.resolve( + RepositoryRestController.BASE_URI.get(), + URI.create(key.substring(1)) + ); + } else { + return key; + } + } + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java index c74d63608..9c713f900 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java @@ -18,9 +18,9 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; -import java.util.Stack; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicReference; +import javax.servlet.http.HttpServletRequest; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; @@ -34,6 +34,7 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -49,7 +50,6 @@ import org.springframework.data.rest.core.Resource; import org.springframework.data.rest.core.ResourceLink; import org.springframework.data.rest.core.ResourceSet; import org.springframework.data.rest.core.convert.DelegatingConversionService; -import org.springframework.data.rest.core.util.UriUtils; import org.springframework.data.rest.repository.AttributeMetadata; import org.springframework.data.rest.repository.EntityMetadata; import org.springframework.data.rest.repository.PageableResourceSet; @@ -59,6 +59,7 @@ import org.springframework.data.rest.repository.RepositoryExporter; import org.springframework.data.rest.repository.RepositoryExporterSupport; import org.springframework.data.rest.repository.RepositoryMetadata; import org.springframework.data.rest.repository.RepositoryNotFoundException; +import org.springframework.data.rest.repository.UriToDomainObjectResolver; import org.springframework.data.rest.repository.annotation.ConvertWith; import org.springframework.data.rest.repository.annotation.RestResource; import org.springframework.data.rest.repository.context.AfterDeleteEvent; @@ -82,12 +83,9 @@ import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.ByteArrayHttpMessageConverter; -import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -135,43 +133,36 @@ public class RepositoryRestController implements ApplicationContextAware, InitializingBean { - public static final String LOCATION = "Location"; - public static final String SELF = "self"; - public static final String LINKS = "_links"; - - private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class); - - private ApplicationContext applicationContext; + public static final String LOCATION = "Location"; + public static final String SELF = "self"; + final static ThreadLocal BASE_URI = new ThreadLocal(); + private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class); /** * We manage a list of possible {@link ConversionService}s to handle converting objects in the controller. This list * is prioritized as well, so one can add a ConversionService at index 0 to make sure that ConversionService takes * priority whenever an object of the type it can convert is needing conversion. */ - @Autowired(required = false) private DelegatingConversionService conversionService = new DelegatingConversionService( new DefaultFormattingConversionService() ); /** * Converters for reading and writing representations of objects. */ - @Autowired(required = false) private List httpMessageConverters = new ArrayList(); /** * List of {@link MediaType}s we can support, given the list of {@link HttpMessageConverter}s currently configured. */ private SortedSet availableMediaTypes = new TreeSet(); - @Autowired(required = false) private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; private ObjectMapper objectMapper = new ObjectMapper(); + private RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter; + private UriToDomainObjectResolver domainObjectResolver; + private ApplicationContext applicationContext; { List httpMessageConverters = new ArrayList(); - httpMessageConverters.add(0, new StringHttpMessageConverter()); - httpMessageConverters.add(0, new ByteArrayHttpMessageConverter()); - httpMessageConverters.add(0, new FormHttpMessageConverter()); - httpMessageConverters.add(0, JacksonUtil.createJacksonHttpMessageConverter(objectMapper)); - httpMessageConverters.add(0, new UriListHttpMessageConverter()); + httpMessageConverters.add(new UriListHttpMessageConverter()); setHttpMessageConverters(httpMessageConverters); } @@ -195,6 +186,7 @@ public class RepositoryRestController * * @param conversionService */ + @Autowired(required = false) public void setConversionService(ConversionService conversionService) { if(null != conversionService) { this.conversionService.addConversionServices(conversionService); @@ -243,10 +235,14 @@ public class RepositoryRestController this.httpMessageConverters = httpMessageConverters; this.availableMediaTypes.clear(); for(HttpMessageConverter conv : httpMessageConverters) { - availableMediaTypes.addAll(conv.getSupportedMediaTypes()); + for(MediaType mt : (List)conv.getSupportedMediaTypes()) { + availableMediaTypes.add(mt.toString()); + } } for(HttpMessageConverter conv : config.getCustomConverters()) { - availableMediaTypes.addAll(conv.getSupportedMediaTypes()); + for(MediaType mt : (List)conv.getSupportedMediaTypes()) { + availableMediaTypes.add(mt.toString()); + } } } @@ -287,17 +283,40 @@ public class RepositoryRestController * * @return @this */ + @Autowired(required = false) public RepositoryRestController setRepositoryRestConfig(RepositoryRestConfiguration config) { this.config = config; return this; } + public RepositoryAwareMappingHttpMessageConverter getMappingHttpMessageConverter() { + return mappingHttpMessageConverter; + } + + @Autowired + public RepositoryRestController setMappingHttpMessageConverter(RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter) { + this.mappingHttpMessageConverter = mappingHttpMessageConverter; + httpMessageConverters.add(mappingHttpMessageConverter); + this.objectMapper = mappingHttpMessageConverter.getObjectMapper(); + return this; + } + + public UriToDomainObjectResolver getDomainObjectResolver() { + return domainObjectResolver; + } + + @Autowired + public RepositoryRestController setDomainObjectResolver(UriToDomainObjectResolver domainObjectResolver) { + this.domainObjectResolver = domainObjectResolver; + return this; + } + @SuppressWarnings({"unchecked"}) @Override public void afterPropertiesSet() throws Exception { - for(ConversionService convsvc : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, - ConversionService.class) - .values()) { - conversionService.addConversionServices(convsvc); + for(ConversionService cs : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, + ConversionService.class) + .values()) { + conversionService.addConversionServices(cs); } } @@ -320,6 +339,7 @@ public class RepositoryRestController public ResponseEntity listRepositories(ServletServerHttpRequest request, UriComponentsBuilder uriBuilder) throws IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); ResourceSet resources = new ResourceSet(); for(RepositoryExporter repoExporter : repositoryExporters) { @@ -361,6 +381,7 @@ public class RepositoryRestController UriComponentsBuilder uriBuilder, @PathVariable String repository) throws IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); if(!repoMeta.exportsMethod(CrudMethod.FIND_ALL)) { @@ -378,8 +399,10 @@ public class RepositoryRestController } // Set page counts in the response - pr.setPaging(new PagingMetadata(page.getNumber() + 1, page.getTotalPages())); - pr.setResourceCount(page.getTotalElements()); + pr.setPaging(new PagingMetadata(page.getNumber() + 1, + page.getSize(), + page.getTotalPages(), + page.getTotalElements())); // Copy over parameters UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository); @@ -470,6 +493,7 @@ public class RepositoryRestController UriComponentsBuilder uriBuilder, @PathVariable String repository) throws IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); ResourceSet resources = new ResourceSet(); @@ -533,6 +557,7 @@ public class RepositoryRestController IllegalAccessException, IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); Repository repo = repoMeta.repository(); @@ -624,8 +649,10 @@ public class RepositoryRestController // Set page counts in the response PageableResourceSet pr = new PageableResourceSet(); - pr.setResourceCount(page.getTotalElements()); - pr.setPaging(new PagingMetadata(page.getNumber() + 1, page.getTotalPages())); + pr.setPaging(new PagingMetadata(page.getNumber() + 1, + page.getSize(), + page.getTotalPages(), + page.getTotalElements())); // Copy over parameters UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository, "search", query); @@ -719,6 +746,7 @@ public class RepositoryRestController UriComponentsBuilder uriBuilder, @PathVariable String repository) throws IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) { @@ -783,6 +811,7 @@ public class RepositoryRestController @PathVariable String repository, @PathVariable String id) throws IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) { @@ -853,6 +882,7 @@ public class RepositoryRestController IllegalAccessException, InstantiationException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE) || !repoMeta.exportsMethod(CrudMethod.FIND_ONE)) { @@ -993,6 +1023,7 @@ public class RepositoryRestController @PathVariable String id, @PathVariable String property) throws IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); String accept = request.getServletRequest().getHeader("Accept"); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); @@ -1025,7 +1056,6 @@ public class RepositoryRestController return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null); } - Object propVal; if(null == (propVal = attrMeta.get(entity))) { return notFoundResponse(request); @@ -1058,17 +1088,13 @@ public class RepositoryRestController } body = resources; } else if(propVal instanceof Map) { - Map resource = new HashMap(); + Map resource = new HashMap(); for(Map.Entry entry : ((Map)propVal).entrySet()) { String propValId = idAttr.get(entry.getValue()).toString(); URI path = buildUri(baseUri, repository, id, property, propValId); + Object oKey = entry.getKey(); - String sKey; - if(ClassUtils.isAssignable(oKey.getClass(), String.class)) { - sKey = (String)oKey; - } else { - sKey = conversionService.convert(oKey, String.class); - } + String sKey = objectToMapKey(oKey); if(shouldReturnLinks(accept)) { resource.put(sKey, new ResourceLink(propertyRel, path)); @@ -1136,6 +1162,7 @@ public class RepositoryRestController @PathVariable String id, final @PathVariable String property) throws IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); final RepositoryMetadata repoMeta = repositoryMetadataFor(repository); if(!repoMeta.exportsMethod(CrudMethod.SAVE_ONE)) { @@ -1200,7 +1227,7 @@ public class RepositoryRestController LinkList.class); for(Link l : incomingLinks.getLinks()) { Object o; - if(null != (o = resolveTopLevelResource(baseUri, l.href().toString()))) { + if(null != (o = domainObjectResolver.resolve(baseUri, URI.create(l.href().toString())))) { rel.set(l.rel()); ResponseEntity possibleResponse = entityHandler.handle(o); if(null != possibleResponse) { @@ -1302,6 +1329,7 @@ public class RepositoryRestController @PathVariable String property, @PathVariable String linkedId) throws IOException { URI baseUri = uriBuilder.build().toUri(); + BASE_URI.set(baseUri); RepositoryMetadata repoMeta = repositoryMetadataFor(repository); if(!repoMeta.exportsMethod(CrudMethod.FIND_ONE)) { @@ -1531,10 +1559,10 @@ public class RepositoryRestController * @throws IOException */ @SuppressWarnings({"unchecked"}) - @ExceptionHandler(OptimisticLockingFailureException.class) + @ExceptionHandler({OptimisticLockingFailureException.class, DataIntegrityViolationException.class}) @ResponseBody - public ResponseEntity handleLockingFailure(OptimisticLockingFailureException ex, - ServletServerHttpRequest request) throws IOException { + public ResponseEntity handleConflict(Exception ex, + ServletServerHttpRequest request) throws IOException { LOG.error(ex.getMessage(), ex); return errorResponse(request, HttpStatus.CONFLICT, ex); } @@ -1579,15 +1607,15 @@ public class RepositoryRestController @SuppressWarnings({"unchecked"}) @ExceptionHandler({HttpMessageNotReadableException.class, HttpMessageNotWritableException.class}) @ResponseBody - public ResponseEntity handleMessageConversionFailure(HttpMessageNotReadableException ex, - ServletServerHttpRequest request) throws IOException { + public ResponseEntity handleMessageConversionFailure(Exception ex, + HttpServletRequest request) throws IOException { LOG.error(ex.getMessage(), ex); Map m = new HashMap(); m.put("message", ex.getMessage()); m.put("acceptableTypes", availableMediaTypes); - return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), m); + return negotiateResponse(new ServletServerHttpRequest(request), HttpStatus.BAD_REQUEST, new HttpHeaders(), m); } /* @@ -1599,18 +1627,6 @@ public class RepositoryRestController return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri(); } - @SuppressWarnings({"unchecked"}) - private URI addSelfLink(URI baseUri, Map model, String... pathComponents) { - List links = (List)model.get(LINKS); - if(null == links) { - links = new ArrayList(); - model.put(LINKS, links); - } - URI selfUri = buildUri(baseUri, pathComponents); - links.add(new ResourceLink(SELF, selfUri)); - return selfUri; - } - @SuppressWarnings({"unchecked"}) private void maybeAddPrevNextLink(URI resourceUri, RepositoryMetadata repoMeta, @@ -1638,38 +1654,6 @@ public class RepositoryRestController } } - @SuppressWarnings({"unchecked"}) - private Object resolveTopLevelResource(URI baseUri, String uri) { - URI href = URI.create(uri); - - URI relativeUri = baseUri.relativize(href); - Stack uris = UriUtils.explode(baseUri, relativeUri); - - if(uris.size() > 1) { - String repoName = UriUtils.path(uris.get(0)); - String sId = UriUtils.path(uris.get(1)); - - RepositoryMetadata repoMeta = repositoryMetadataFor(repoName); - - CrudRepository repo; - if(null == (repo = repoMeta.repository())) { - return null; - } - - EntityMetadata entityMeta; - if(null == (entityMeta = repoMeta.entityMetadata())) { - return null; - } - - Class idType = (Class)entityMeta.idAttribute().type(); - Serializable serId = stringToSerializable(sId, idType); - - return repo.findOne(serId); - } - - return null; - } - @SuppressWarnings({"unchecked"}) private V readIncoming(HttpInputMessage request, MediaType incomingMediaType, Class targetType) throws IOException { @@ -1685,7 +1669,7 @@ public class RepositoryRestController return (V)conv.read(targetType, request); } } - return null; + return (V)mappingHttpMessageConverter.read(targetType, request); } private MapResource createResource(String repoRel, @@ -1758,6 +1742,24 @@ public class RepositoryRestController return m; } + private String objectToMapKey(Object obj) { + Assert.notNull(obj, "Map key cannot be null!"); + + RepositoryMetadata repoMeta; + String key; + if(ClassUtils.isAssignable(obj.getClass(), String.class)) { + key = (String)obj; + } else if(null != (repoMeta = repositoryMetadataFor(obj.getClass()))) { + AttributeMetadata attrMeta = repoMeta.entityMetadata().idAttribute(); + String id = attrMeta.get(obj).toString(); + key = "@" + buildUri(BASE_URI.get(), repoMeta.name(), id); + } else { + key = conversionService.convert(obj, String.class); + } + + return key; + } + private ResponseEntity notFoundResponse(ServletServerHttpRequest request) throws IOException { return negotiateResponse(request, HttpStatus.NOT_FOUND, new HttpHeaders(), null); } @@ -1804,7 +1806,7 @@ public class RepositoryRestController headers.setContentType(acceptType); if(null == converter) { - throw new HttpMessageNotWritableException("No HttpMessageConverter found to handle " + resource.getClass()); + converter = mappingHttpMessageConverter; } final ByteArrayOutputStream bout = new ByteArrayOutputStream(); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java index 64c5bd863..0acaf2cbe 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java @@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; +import org.springframework.data.rest.repository.UriToDomainObjectResolver; import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener; import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; @@ -80,6 +81,26 @@ public class RepositoryRestMvcConfiguration { } } + /** + * Special Repository-aware {@link org.springframework.http.converter.HttpMessageConverter} that can deal with + * entities and links. + * + * @return + */ + @Bean public RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter() { + return new RepositoryAwareMappingHttpMessageConverter(); + } + + /** + * A {@link org.springframework.data.rest.core.Resolver} implementation that takes a {@link java.net.URI} and turns it + * into a top-level domain object. + * + * @return + */ + @Bean public UriToDomainObjectResolver domainObjectResolver() { + return new UriToDomainObjectResolver(); + } + /** * The main REST exporter Spring MVC controller. * @@ -87,8 +108,7 @@ public class RepositoryRestMvcConfiguration { * * @throws Exception */ - @Bean public RepositoryRestController repositoryRestController() - throws Exception { + @Bean public RepositoryRestController repositoryRestController() throws Exception { return new RepositoryRestController(); } diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy index 1574f0578..1fb66b669 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/DiscoverySpec.groovy @@ -43,8 +43,8 @@ class DiscoverySpec extends BaseSpec { then: response.statusCode == HttpStatus.OK body.content.size() == 10 - body.page.total == 2 - body.page.current == 1 + body.page.totalPages == 2 + body.page.number == 1 } diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy index 7ce8d1d18..e800db32b 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy @@ -1,11 +1,8 @@ package org.springframework.data.rest.webmvc.spec import org.codehaus.jackson.map.ObjectMapper -import org.codehaus.jackson.map.ser.CustomSerializerFactory import org.springframework.context.support.ClassPathXmlApplicationContext import org.springframework.data.domain.PageRequest - -import org.springframework.data.rest.core.util.FluentBeanSerializer import org.springframework.data.rest.test.webmvc.Address import org.springframework.data.rest.webmvc.PagingAndSorting import org.springframework.data.rest.webmvc.RepositoryRestConfiguration @@ -26,7 +23,6 @@ import spock.lang.Shared import spock.lang.Specification import javax.persistence.EntityManagerFactory -import org.springframework.data.rest.core.ResourceLink /** * @author Jon Brisbin @@ -73,10 +69,6 @@ class RepositoryRestControllerSpec extends Specification { controller = webAppCtx.getBean(RepositoryRestController) pageSort = new PagingAndSorting(RepositoryRestConfiguration.DEFAULT, new PageRequest(0, 1000)) uriBuilder = UriComponentsBuilder.fromUriString("http://localhost:8080/data") - - def customSerializerFactory = new CustomSerializerFactory() - customSerializerFactory.addSpecificMapping(ResourceLink, new FluentBeanSerializer(ResourceLink)) - mapper.setSerializerFactory(customSerializerFactory) } def setup() { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/WebCustomer.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/WebCustomer.java index 0aaf2b731..8375c6a86 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/WebCustomer.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/WebCustomer.java @@ -1,6 +1,8 @@ package org.springframework.data.rest.test.webmvc; +import java.util.Map; import javax.persistence.Entity; +import javax.persistence.OneToMany; /** * @author Jon Brisbin @@ -8,7 +10,9 @@ import javax.persistence.Entity; @Entity public class WebCustomer extends Customer { - private String username; + private String username; + @OneToMany + private Map people; public String getUsername() { return username; @@ -18,4 +22,12 @@ public class WebCustomer extends Customer { this.username = username; } + public Map getPeople() { + return people; + } + + public void setPeople(Map people) { + this.people = people; + } + }