Re-organized how entities are serialized. Broke out the Jackson Module into its own class and incorporated the code from EntityResource.wrap into the serializer so that entities are properly serialized wherever they are encountered. Also implemented a Converter and use the ConversionService to turn an Entity into a Resource. Right now that's done in two separate places since the propertyOfEntity method needs to turn entities into resources as well as the Jackson serializer. Also threw in a fix for DATAREST-43.

This commit is contained in:
Jon Brisbin
2012-08-30 09:38:28 -05:00
committed by Jon Brisbin
parent 40645673bf
commit 0f925cc5ff
7 changed files with 525 additions and 365 deletions

View File

@@ -0,0 +1,63 @@
package org.springframework.data.rest.webmvc;
import static org.springframework.data.rest.core.util.UriUtils.*;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.rest.repository.AttributeMetadata;
import org.springframework.data.rest.repository.EntityMetadata;
import org.springframework.data.rest.repository.RepositoryMetadata;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
/**
* A {@link Converter} to turn domain entities into {@link Resource}s by segregating embedded entities (those entities
* not managed by a {@link org.springframework.data.repository.Repository}) from linked or related entities (which
* don't get inlined into an entity's representation but are replaced by links instead.
*
* @author Jon Brisbin
*/
public class EntityToResourceConverter implements Converter<Object, Resource> {
private final RepositoryMetadata repositoryMetadata;
private final EntityMetadata entityMetadata;
public EntityToResourceConverter(RepositoryMetadata repositoryMetadata) {
this.repositoryMetadata = repositoryMetadata;
this.entityMetadata = repositoryMetadata.entityMetadata();
}
@SuppressWarnings({"unchecked"})
@Override public Resource convert(Object source) {
if(null == repositoryMetadata || null == source) {
return new Resource<Object>(source);
}
URI baseUri = RepositoryRestController.BASE_URI.get();
Set<Link> links = new HashSet<Link>();
for(Object attrName : entityMetadata.linkedAttributes().keySet()) {
URI uri = buildUri(baseUri, attrName.toString());
String rel = repositoryMetadata.rel() + "." + source.getClass().getSimpleName() + "." + attrName;
links.add(new Link(uri.toString(), rel));
}
links.add(new Link(baseUri.toString(), "self"));
Map<String, Object> entityDto = new HashMap<String, Object>();
for(Map.Entry<String, AttributeMetadata> attrMeta : ((Map<String, AttributeMetadata>)entityMetadata.embeddedAttributes())
.entrySet()) {
String name = attrMeta.getKey();
Object val;
if(null != (val = attrMeta.getValue().get(source))) {
entityDto.put(name, val);
}
}
return new EntityResource(entityDto, links);
}
}

View File

@@ -1,54 +1,31 @@
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.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.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.convert.ConversionService;
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.UriToDomainObjectUriResolver;
import org.springframework.data.rest.webmvc.json.RepositoryAwareJacksonModule;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.web.util.UriComponentsBuilder;
/**
* @author Jon Brisbin
@@ -67,6 +44,8 @@ public class RepositoryAwareMappingHttpMessageConverter
protected List<Module> modules = Collections.emptyList();
@Autowired
protected UriToDomainObjectUriResolver domainObjectResolver = null;
@Autowired
protected RepositoryAwareJacksonModule jacksonModule = null;
protected ApplicationEventPublisher eventPublisher = null;
public RepositoryAwareMappingHttpMessageConverter() {
@@ -83,7 +62,7 @@ public class RepositoryAwareMappingHttpMessageConverter
}
@Override public void afterPropertiesSet() throws Exception {
mapper.registerModule(new RepositoryAwareModule());
mapper.registerModule(jacksonModule);
for(Module m : modules) {
mapper.registerModule(m);
}
@@ -102,11 +81,9 @@ public class RepositoryAwareMappingHttpMessageConverter
return repositoryExporters;
}
@Autowired(required = false)
@SuppressWarnings({"unchecked"})
public RepositoryAwareMappingHttpMessageConverter setRepositoryExporters(List<RepositoryExporter> repositoryExporters) {
this.repositoryExporters = repositoryExporters;
this.mapper.registerModule(new RepositoryAwareModule());
return this;
}
@@ -182,298 +159,4 @@ public class RepositoryAwareMappingHttpMessageConverter
}
}
@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 {
SimpleSerializers sers = new SimpleSerializers();
SimpleDeserializers dsers = new SimpleDeserializers();
SimpleSerializers keySers = new SimpleSerializers();
SimpleKeyDeserializers keyDsers = new SimpleKeyDeserializers();
private RepositoryAwareModule() {
super("RepositoryAwareModule", Version.unknownVersion());
}
@SuppressWarnings({"unchecked"})
@Override public void setupModule(SetupContext context) {
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 = null;
for(ConversionService cs : conversionServices) {
if(cs.canConvert(idAttr.type(), String.class)) {
sId = cs.convert(serId, String.class);
break;
}
}
if(null == sId) {
sId = serId.toString();
}
String rel = repoMeta.rel() + "." + repoMeta.domainType().getSimpleName();
URI selfUri = buildUri(RepositoryRestController.BASE_URI.get(), repoMeta.name(), sId);
jgen.writeObject(new Link(selfUri.toString(), rel));
}
}
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 = null;
for(ConversionService cs : conversionServices) {
if(cs.canConvert(idAttr.type(), String.class)) {
sId = cs.convert(serId, String.class);
break;
}
}
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.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) {
String name = jp.getCurrentName();
switch(tok) {
case FIELD_NAME: {
// Read the attribute metadata
AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(name);
Object val = null;
if(name.startsWith("@http")) {
entity = domainObjectResolver.resolve(
RepositoryRestController.BASE_URI.get(),
URI.create(name.substring(1))
);
continue;
}
if("href".equals(name)) {
entity = domainObjectResolver.resolve(
RepositoryRestController.BASE_URI.get(),
URI.create(jp.nextTextValue())
);
continue;
}
if("rel".equals(name)) {
// rel is currently ignored
continue;
}
if(null == attrMeta) {
// do nothing
continue;
}
// Try and read the value of this attribute.
// The method of doing that varies based on the type of the property.
if(attrMeta.isCollectionLike()) {
Collection c = attrMeta.asCollection(entity);
if(null == c || c == Collections.emptyList()) {
c = new ArrayList();
}
if((tok = jp.nextToken()) == JsonToken.START_ARRAY) {
do {
Object cval = jp.readValueAs(attrMeta.elementType());
c.add(cval);
} while((tok = jp.nextToken()) != JsonToken.END_ARRAY);
val = c;
} else if(tok == JsonToken.VALUE_NULL) {
val = null;
} else {
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Collection.");
}
} else if(attrMeta.isSetLike()) {
Set s = attrMeta.asSet(entity);
if(null == s || s == Collections.emptySet()) {
s = new HashSet();
}
if((tok = jp.nextToken()) == JsonToken.START_ARRAY) {
do {
Object sval = jp.readValueAs(attrMeta.elementType());
s.add(sval);
} while((tok = jp.nextToken()) != JsonToken.END_ARRAY);
val = s;
} else if(tok == JsonToken.VALUE_NULL) {
val = null;
} else {
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Set.");
}
} else if(attrMeta.isMapLike()) {
Map m = attrMeta.asMap(entity);
if(null == m || m == Collections.emptyMap()) {
m = new HashMap();
}
if((tok = jp.nextToken()) == JsonToken.START_OBJECT) {
do {
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);
} while((tok = jp.nextToken()) != JsonToken.END_OBJECT);
val = m;
} else if(tok == JsonToken.VALUE_NULL) {
val = null;
} else {
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Map.");
}
} else {
if((tok = jp.nextToken()) != JsonToken.VALUE_NULL) {
val = jp.readValueAs(attrMeta.type());
}
}
if(null != val) {
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;
}
}
}
}

View File

@@ -38,6 +38,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Page;
@@ -130,12 +131,13 @@ public class RepositoryRestController
implements ApplicationContextAware,
InitializingBean {
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(
public static final String LOCATION = "Location";
public static final String SELF = "self";
public static final ThreadLocal<URI> BASE_URI = new ThreadLocal<URI>();
private static final Logger LOG = LoggerFactory.getLogger(
RepositoryRestController.class);
private static final TypeDescriptor STRING_ARRAY_TYPE = TypeDescriptor.valueOf(String[].class);
private static final TypeDescriptor STRING_ARRAY_TYPE = TypeDescriptor.valueOf(String[].class);
/**
* We manage a list of possible {@link ConversionService}s to handle converting objects in the controller. This list
@@ -326,6 +328,16 @@ public class RepositoryRestController
.values()) {
conversionService.addConversionServices(cs);
}
GenericConversionService entityConverters = new GenericConversionService();
for(RepositoryExporter exp : repositoryExporters()) {
for(String repoName : (Set<String>)exp.repositoryNames()) {
RepositoryMetadata repoMeta = exp.repositoryMetadataFor(repoName);
Class<?> domainType = repoMeta.domainType();
entityConverters.addConverter(domainType, Resource.class, new EntityToResourceConverter(repoMeta));
}
}
conversionService.addConversionService(0, entityConverters);
}
/**
@@ -397,7 +409,7 @@ public class RepositoryRestController
return negotiateResponse(request, HttpStatus.METHOD_NOT_ALLOWED, new HttpHeaders(), null);
}
Set<Link> links = new HashSet<Link>();
List<Link> links = new ArrayList<Link>();
PagedResources.PageMetadata pageMeta = null;
Iterator allEntities = Collections.emptyList().iterator();
@@ -452,16 +464,15 @@ public class RepositoryRestController
}
}
List<Resource<Map<String, Object>>> allResources = new ArrayList<Resource<Map<String, Object>>>();
List allResources = new ArrayList();
while(allEntities.hasNext()) {
Object o = allEntities.next();
Serializable id = (Serializable)repoMeta.entityMetadata().idAttribute().get(o);
if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) {
Serializable id = (Serializable)repoMeta.entityMetadata().idAttribute().get(o);
links.add(new Link(buildUri(baseUri, repository, id.toString()).toString(),
repoMeta.rel() + "." + o.getClass().getSimpleName()));
} else {
URI selfUri = buildUri(baseUri, repository, id.toString());
allResources.add(EntityResource.wrap(o, repoMeta, selfUri));
allResources.add(o);
}
}
@@ -687,23 +698,19 @@ public class RepositoryRestController
List<Object> results = new ArrayList<Object>();
while(entities.hasNext()) {
Object obj = entities.next();
if(!hasRepositoryMetadataFor(obj.getClass())) {
results.add(obj);
continue;
}
// This object is managed by a repository
RepositoryMetadata elemRepoMeta = repositoryMetadataFor(obj.getClass());
String id = elemRepoMeta.entityMetadata().idAttribute().get(obj).toString();
if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) {
if(!hasRepositoryMetadataFor(obj.getClass())) {
results.add(obj);
continue;
}
// This object is managed by a repository
RepositoryMetadata elemRepoMeta = repositoryMetadataFor(obj.getClass());
String id = elemRepoMeta.entityMetadata().idAttribute().get(obj).toString();
String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName();
URI path = buildUri(baseUri, repository, id);
links.add(new org.springframework.hateoas.Link(path.toString(), rel));
} else {
URI selfUri = buildUri(baseUri, repository, id);
results.add(EntityResource.wrap(obj, repoMeta, selfUri));
results.add(obj);
}
}
@@ -767,7 +774,7 @@ public class RepositoryRestController
Resource<?> body = null;
if(returnBody(request)) {
body = EntityResource.wrap(savedEntity, repoMeta, selfUri);
body = new Resource<Object>(savedEntity);
}
return negotiateResponse(request, HttpStatus.CREATED, headers, body);
@@ -826,11 +833,10 @@ public class RepositoryRestController
}
}
URI selfUri = buildUri(baseUri, repository, id);
return negotiateResponse(request,
HttpStatus.OK,
headers,
EntityResource.wrap(entity, repoMeta, selfUri));
entity);
}
/**
@@ -1049,10 +1055,14 @@ public class RepositoryRestController
if(shouldReturnLinks(accept)) {
links.add(new Link(path.toString(), propertyRel));
} else {
URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId);
EntityResource er = EntityResource.wrap(o, propRepoMeta, selfUri);
er.add(new Link(path.toString(), propertyRel));
outgoing.add(er);
Resource r;
if(conversionService.canConvert(o.getClass(), Resource.class)) {
r = conversionService.convert(o, Resource.class);
} else {
r = new Resource(o);
}
r.add(new Link(path.toString(), propertyRel));
outgoing.add(r);
}
}
@@ -1074,9 +1084,14 @@ public class RepositoryRestController
if(shouldReturnLinks(accept)) {
resource.put(sKey, new Link(path.toString(), propertyRel));
} else {
URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId);
EntityResource er = EntityResource.wrap(entry.getValue(), propRepoMeta, selfUri);
resource.put(sKey, er);
Resource r;
if(conversionService.canConvert(entry.getValue().getClass(), Resource.class)) {
r = conversionService.convert(entry.getValue(), Resource.class);
} else {
r = new Resource(entry.getValue());
}
r.add(new Link(path.toString(), propertyRel));
resource.put(sKey, r);
}
}
@@ -1086,22 +1101,32 @@ public class RepositoryRestController
new Resource(resource, links));
} else {
String propValId = idAttr.get(propVal).toString();
URI path = buildUri(baseUri, repository, id, property);
URI selfUri = buildUri(baseUri, propRepoMeta.name(), propValId);
List<Resource<?>> outgoing = new ArrayList<Resource<?>>();
if(shouldReturnLinks(accept)) {
links.add(new Link(path.toString(), propertyRel));
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
new Resources(Collections.emptyList(), links));
} else {
EntityResource er = EntityResource.wrap(propVal, propRepoMeta, selfUri);
outgoing.add(er);
Resource r;
if(conversionService.canConvert(propVal.getClass(), Resource.class)) {
r = conversionService.convert(propVal, Resource.class);
} else {
r = new Resource(propVal);
}
r.add(new Link(path.toString(), propertyRel));
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
r);
}
return negotiateResponse(request,
HttpStatus.OK,
new HttpHeaders(),
new Resources(outgoing, links));
}
@@ -1622,7 +1647,7 @@ public class RepositoryRestController
boolean addIf,
int nextPage,
String rel,
Set<Link> links) {
Collection<Link> links) {
if(null != page && addIf) {
UriComponentsBuilder urib = UriComponentsBuilder.fromUri(resourceUri);
urib.queryParam(config.getPageParamName(), nextPage); // PageRequest is 0-based, so it's already (page - 1)

View File

@@ -9,6 +9,7 @@ import org.springframework.context.annotation.ImportResource;
import org.springframework.data.rest.repository.UriToDomainObjectUriResolver;
import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener;
import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter;
import org.springframework.data.rest.webmvc.json.RepositoryAwareJacksonModule;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
@@ -77,6 +78,15 @@ public class RepositoryRestMvcConfiguration {
: validatingRepositoryEventListener);
}
/**
* A special Jackson {@link org.codehaus.jackson.map.Module} implementation that configures converters for entities.
*
* @return
*/
@Bean public RepositoryAwareJacksonModule jacksonModule() {
return new RepositoryAwareJacksonModule();
}
/**
* Special Repository-aware {@link org.springframework.http.converter.HttpMessageConverter} that can deal with
* entities and links.
@@ -88,7 +98,8 @@ public class RepositoryRestMvcConfiguration {
}
/**
* A {@link org.springframework.data.rest.core.UriResolver} implementation that takes a {@link java.net.URI} and turns
* A {@link org.springframework.data.rest.core.UriResolver} implementation that takes a {@link java.net.URI} and
* turns
* it
* into a top-level domain object.
*

View File

@@ -1,4 +1,4 @@
package org.springframework.data.rest.webmvc;
package org.springframework.data.rest.webmvc.json;
import java.io.IOException;
import java.util.Arrays;
@@ -6,6 +6,7 @@ import java.util.Arrays;
import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.data.rest.webmvc.MediaTypes;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotWritableException;

View File

@@ -0,0 +1,364 @@
package org.springframework.data.rest.webmvc.json;
import static org.springframework.data.rest.core.util.UriUtils.*;
import static org.springframework.data.util.ClassTypeInformation.*;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
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 com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
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.SerializerProvider;
import org.codehaus.jackson.map.deser.std.StdDeserializer;
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.BeanUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.GenericConversionService;
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.UriToDomainObjectUriResolver;
import org.springframework.data.rest.webmvc.EntityToResourceConverter;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.util.TypeInformation;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.http.converter.HttpMessageNotReadableException;
/**
* Special implementation of a Jackson {@link org.codehaus.jackson.map.Module} to handle properly serializing and
* deserializing entities with links.
*
* @author Jon Brisbin
*/
public class RepositoryAwareJacksonModule extends SimpleModule implements InitializingBean {
@Autowired(required = false)
protected List<RepositoryExporter> repositoryExporters = Collections.emptyList();
@Autowired(required = false)
private List<ConversionService> conversionServices = Collections.emptyList();
@Autowired(required = false)
private List<ResourceProcessor<Resource<?>>> resourceProcessors = Collections.emptyList();
private Multimap<Class<?>, ResourceProcessor<Resource<?>>> resourceProcessorMap = ArrayListMultimap.create();
@Autowired
private UriToDomainObjectUriResolver domainObjectResolver;
private final GenericConversionService conversionService = new GenericConversionService();
private final SimpleSerializers sers = new SimpleSerializers();
private final SimpleDeserializers dsers = new SimpleDeserializers();
private final SimpleSerializers keySers = new SimpleSerializers();
private final SimpleKeyDeserializers keyDsers = new SimpleKeyDeserializers();
public RepositoryAwareJacksonModule() {
super("RepositoryAwareJacksonModule", Version.unknownVersion());
}
@SuppressWarnings({"unchecked"})
@Override public void afterPropertiesSet() throws Exception {
for(RepositoryExporter repoExp : repositoryExporters) {
for(String repoName : new ArrayList<String>(repoExp.repositoryNames())) {
RepositoryMetadata repoMeta = repoExp.repositoryMetadataFor(repoName);
Class domainType = repoMeta.entityMetadata().type();
TypeInformation<?> domainTypeInfo = from(domainType);
for(ResourceProcessor<Resource<?>> rp : resourceProcessors) {
TypeInformation<?> resourceType = from(rp.getClass())
.getSuperTypeInformation(ResourceProcessor.class)
.getComponentType();
Class<?> processorType = resourceType.getType();
TypeInformation<?> componentType = resourceType.getComponentType();
if(Resource.class.isAssignableFrom(processorType) && componentType.isAssignableFrom(domainTypeInfo)) {
resourceProcessorMap.put(domainType, rp);
}
}
if(!conversionService.canConvert(domainType, Resource.class)) {
// Assign only if no custom converter already assigned
conversionService.addConverter(domainType, Resource.class, new EntityToResourceConverter(repoMeta));
}
sers.addSerializer(domainType, new DomainObjectToResourceSerializer(domainType));
keySers.addSerializer(domainType, new DomainObjectToStringKeySerializer(domainType, repoMeta));
dsers.addDeserializer(domainType, new LinkToDomainObjectDeserializer(domainType, repoMeta));
keyDsers.addDeserializer(domainType, new KeyToDomainObjectDeserializer());
}
}
}
@Override public void setupModule(SetupContext context) {
context.addSerializers(sers);
context.addKeySerializers(keySers);
context.addDeserializers(dsers);
context.addKeyDeserializers(keyDsers);
}
private class DomainObjectToResourceSerializer extends SerializerBase<Object> {
private DomainObjectToResourceSerializer(Class<Object> t) {
super(t);
}
@SuppressWarnings({"unchecked"})
@Override public void serialize(Object value,
JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonGenerationException {
if(null == value) {
provider.defaultSerializeNull(jgen);
return;
}
if(!conversionService.canConvert(value.getClass(), Resource.class)) {
provider.defaultSerializeValue(value, jgen);
return;
}
// Process the resource first to catch user stuff
Resource<?> resource = new Resource<Object>(value);
for(ResourceProcessor<Resource<?>> rp : resourceProcessorMap.get(value.getClass())) {
resource = rp.process(resource);
}
// Maybe convert the resource so we can extract linked properties
if(null == resource.getContent()) {
provider.defaultSerializeNull(jgen);
return;
}
if(conversionService.canConvert(resource.getContent().getClass(), Resource.class)) {
Set<Link> links = resource.getLinks();
resource = conversionService.convert(value, Resource.class);
resource.add(links);
}
jgen.writeObject(resource);
}
}
private class DomainObjectToStringKeySerializer extends SerializerBase<Object> {
private final RepositoryMetadata repoMeta;
private final AttributeMetadata idAttr;
private DomainObjectToStringKeySerializer(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 = null;
for(ConversionService cs : conversionServices) {
if(cs.canConvert(idAttr.type(), String.class)) {
sId = cs.convert(serId, String.class);
break;
}
}
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 = BeanUtils.instantiate(getValueClass());
for(JsonToken tok = jp.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) {
String name = jp.getCurrentName();
switch(tok) {
case FIELD_NAME: {
// Read the attribute metadata
AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute(name);
Object val = null;
if(name.startsWith("@http")) {
entity = domainObjectResolver.resolve(
RepositoryRestController.BASE_URI.get(),
URI.create(name.substring(1))
);
continue;
}
if("href".equals(name)) {
entity = domainObjectResolver.resolve(
RepositoryRestController.BASE_URI.get(),
URI.create(jp.nextTextValue())
);
continue;
}
if("rel".equals(name)) {
// rel is currently ignored
continue;
}
if(null == attrMeta) {
// do nothing
continue;
}
// Try and read the value of this attribute.
// The method of doing that varies based on the type of the property.
if(attrMeta.isCollectionLike()) {
Collection c = attrMeta.asCollection(entity);
if(null == c || c == Collections.emptyList()) {
c = new ArrayList();
}
if((tok = jp.nextToken()) == JsonToken.START_ARRAY) {
do {
Object cval = jp.readValueAs(attrMeta.elementType());
c.add(cval);
} while((tok = jp.nextToken()) != JsonToken.END_ARRAY);
val = c;
} else if(tok == JsonToken.VALUE_NULL) {
val = null;
} else {
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Collection.");
}
} else if(attrMeta.isSetLike()) {
Set s = attrMeta.asSet(entity);
if(null == s || s == Collections.emptySet()) {
s = new HashSet();
}
if((tok = jp.nextToken()) == JsonToken.START_ARRAY) {
do {
Object sval = jp.readValueAs(attrMeta.elementType());
s.add(sval);
} while((tok = jp.nextToken()) != JsonToken.END_ARRAY);
val = s;
} else if(tok == JsonToken.VALUE_NULL) {
val = null;
} else {
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Set.");
}
} else if(attrMeta.isMapLike()) {
Map m = attrMeta.asMap(entity);
if(null == m || m == Collections.emptyMap()) {
m = new HashMap();
}
if((tok = jp.nextToken()) == JsonToken.START_OBJECT) {
do {
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);
} while((tok = jp.nextToken()) != JsonToken.END_OBJECT);
val = m;
} else if(tok == JsonToken.VALUE_NULL) {
val = null;
} else {
throw new HttpMessageNotReadableException("Cannot read a JSON " + tok + " as a Map.");
}
} else {
if((tok = jp.nextToken()) != JsonToken.VALUE_NULL) {
val = jp.readValueAs(attrMeta.type());
}
}
if(null != val) {
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;
}
}
}
}

View File

@@ -14,6 +14,9 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaDialect;
@@ -83,4 +86,14 @@ public class ApplicationConfig {
return cs;
}
@Bean public ResourceProcessor<Resource<Person>> personProcessor() {
return new ResourceProcessor<Resource<Person>>() {
@Override public Resource<Person> process(Resource<Person> resource) {
System.out.println("\t***** ResourceProcessor for Person: " + resource);
resource.add(new Link("http://localhost:8080/people", "added-link"));
return resource;
}
};
}
}