Implemented a new style of JSON serialization and deserialization by registering Repository-aware components with Jackson's underlying ObjectMapper. This has the benefit of allowing us to transliterate domain objects into link representations and back again, no matter where those entities appear in complex nested object graphs.
This commit is contained in:
@@ -9,7 +9,6 @@ import org.codehaus.jackson.annotate.JsonTypeInfo;
|
||||
*
|
||||
* @author Jon Brisbin <jbrisbin@vmware.com>
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, defaultImpl = ResourceLink.class)
|
||||
public interface Link {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<Link> {
|
||||
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;
|
||||
|
||||
@@ -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 <jbrisbin@vmware.com>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <jbrisbin@vmware.com>
|
||||
*/
|
||||
public class FluentBeanSerializer
|
||||
extends SerializerBase<Object> {
|
||||
|
||||
@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<String, Object> entry : ((Map<String, Object>)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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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...).
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
*/
|
||||
public abstract class RepositoryExporterSupport<S extends RepositoryExporterSupport<? super S>> {
|
||||
|
||||
@Autowired(required = false)
|
||||
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
|
||||
|
||||
/**
|
||||
@@ -31,6 +30,7 @@ public abstract class RepositoryExporterSupport<S extends RepositoryExporterSupp
|
||||
* @param repositoryExporters
|
||||
* Export this {@link List} of {@link RepositoryExporter}s.
|
||||
*/
|
||||
@Autowired(required = false)
|
||||
public void setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
|
||||
this.repositoryExporters = repositoryExporters;
|
||||
}
|
||||
|
||||
@@ -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<M extends RepositoryMetadata<E>, E extends EntityMetadata<? extends AttributeMetadata>>
|
||||
implements Resolver<M>,
|
||||
ApplicationContextAware {
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
@Autowired(required = false)
|
||||
private List<RepositoryExporter> repositoryExporters = Collections.emptyList();
|
||||
|
||||
@Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public List<RepositoryExporter> getRepositoryExporters() {
|
||||
return repositoryExporters;
|
||||
}
|
||||
|
||||
public RepositoryMetadataResolver<M, E> setRepositoryExporters(List<RepositoryExporter> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<UriToDomainObjectResolver>
|
||||
implements Resolver<Object> {
|
||||
|
||||
@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<URI> 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<? extends Serializable> idType = (Class<? extends Serializable>)entityMeta.idAttribute().type();
|
||||
Serializable serId;
|
||||
if(ClassUtils.isAssignable(idType, String.class)) {
|
||||
serId = sId;
|
||||
} else {
|
||||
serId = conversionService.convert(sId, idType);
|
||||
}
|
||||
|
||||
return repo.findOne(serId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<RepositoryExporter> repositoryExporters = Collections.emptyList();
|
||||
@Autowired(required = false)
|
||||
protected List<Module> 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<RepositoryExporter> getRepositoryExporters() {
|
||||
return repositoryExporters;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
@SuppressWarnings({"unchecked"})
|
||||
public RepositoryAwareMappingHttpMessageConverter setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
|
||||
this.repositoryExporters = repositoryExporters;
|
||||
|
||||
for(RepositoryExporter repoExp : repositoryExporters) {
|
||||
for(String repoName : new ArrayList<String>(repoExp.repositoryNames())) {
|
||||
RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName);
|
||||
Class domainType = repoMeta.entityMetadata().type();
|
||||
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<Module> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
public RepositoryAwareMappingHttpMessageConverter setModules(List<Module> 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<String>(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<String>(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<Object> {
|
||||
|
||||
protected final RepositoryMetadata repoMeta;
|
||||
protected final AttributeMetadata idAttr;
|
||||
|
||||
private DomainObjectToLinkSerializer(Class<Object> 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<Object> 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<Object> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<URI> BASE_URI = new ThreadLocal<URI>();
|
||||
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<HttpMessageConverter> httpMessageConverters = new ArrayList<HttpMessageConverter>();
|
||||
/**
|
||||
* List of {@link MediaType}s we can support, given the list of {@link HttpMessageConverter}s currently configured.
|
||||
*/
|
||||
private SortedSet<String> availableMediaTypes = new TreeSet<String>();
|
||||
@Autowired(required = false)
|
||||
private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT;
|
||||
private ObjectMapper objectMapper = new ObjectMapper();
|
||||
private RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter;
|
||||
private UriToDomainObjectResolver domainObjectResolver;
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
{
|
||||
List<HttpMessageConverter> httpMessageConverters = new ArrayList<HttpMessageConverter>();
|
||||
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<MediaType>)conv.getSupportedMediaTypes()) {
|
||||
availableMediaTypes.add(mt.toString());
|
||||
}
|
||||
}
|
||||
for(HttpMessageConverter conv : config.getCustomConverters()) {
|
||||
availableMediaTypes.addAll(conv.getSupportedMediaTypes());
|
||||
for(MediaType mt : (List<MediaType>)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<String, Object> resource = new HashMap<String, Object>();
|
||||
Map resource = new HashMap();
|
||||
for(Map.Entry<Object, Object> entry : ((Map<Object, Object>)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<String, Object> model, String... pathComponents) {
|
||||
List<Link> links = (List<Link>)model.get(LINKS);
|
||||
if(null == links) {
|
||||
links = new ArrayList<Link>();
|
||||
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<URI> 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<? extends Serializable> idType = (Class<? extends Serializable>)entityMeta.idAttribute().type();
|
||||
Serializable serId = stringToSerializable(sId, idType);
|
||||
|
||||
return repo.findOne(serId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
private <V> V readIncoming(HttpInputMessage request, MediaType incomingMediaType, Class<V> 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<byte[]> 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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <jbrisbin@vmware.com>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<Profile, Person> people;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
@@ -18,4 +22,12 @@ public class WebCustomer extends Customer {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public Map<Profile, Person> getPeople() {
|
||||
return people;
|
||||
}
|
||||
|
||||
public void setPeople(Map<Profile, Person> people) {
|
||||
this.people = people;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user