#47, #60 - First draft on extended HAL integration.

Collections are now rendered as _embedded in combination with a topic relation (https://groups.google.com/forum/?fromgroups=#!topic/hal-discuss/bCzydTlHB3M
). Adapted deserializers accordingly.
This commit is contained in:
Alexander Bätz
2013-02-11 17:13:27 +01:00
committed by Oliver Gierke
parent 1d1d959143
commit 47b50266b8
8 changed files with 879 additions and 23 deletions

View File

@@ -164,7 +164,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
}
private void registerModule(Object objectMapper) {
((ObjectMapper) objectMapper).registerModule(new Jackson2HalModule());
((ObjectMapper) objectMapper).registerModule(new Jackson2HalModule(null));
}
}
@@ -217,7 +217,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
}
private void registerModule(Object mapper) {
((org.codehaus.jackson.map.ObjectMapper) mapper).registerModule(new Jackson1HalModule());
((org.codehaus.jackson.map.ObjectMapper) mapper).registerModule(new Jackson1HalModule(null));
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012 the original author or authors.
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package org.springframework.hateoas.hal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -24,21 +25,35 @@ import java.util.Map;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParseException;
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.BeanProperty;
import org.codehaus.jackson.map.ContextualDeserializer;
import org.codehaus.jackson.map.ContextualSerializer;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.TypeSerializer;
import org.codehaus.jackson.map.deser.std.ContainerDeserializerBase;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.map.module.SimpleSerializers;
import org.codehaus.jackson.map.ser.std.ContainerSerializerBase;
import org.codehaus.jackson.map.ser.std.MapSerializer;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.util.StringUtils;
/**
* Jackson 1 module implementation to render {@link Link} and {@link ResourceSupport} instances in HAL compatible JSON.
@@ -51,16 +66,23 @@ public class Jackson1HalModule extends SimpleModule {
/**
* Creates a new {@link Jackson1HalModule}.
*/
public Jackson1HalModule() {
public Jackson1HalModule(RelProvider relProvider) {
super("json-hal-module", new Version(1, 0, 0, null));
setMixInAnnotation(Link.class, LinkMixin.class);
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
setMixInAnnotation(Resources.class, ResourcesMixin.class);
SimpleSerializers serializers = new SimpleSerializers();
serializers.addSerializer(new HalResourcesSerializer(relProvider));
setSerializers(serializers);
}
/**
* Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON.
* Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. Renders the list as a map, where
* links are sorted based on their relation.
*
* @author Alexander Baetz
* @author Oliver Gierke
@@ -113,7 +135,7 @@ public class Jackson1HalModule extends SimpleModule {
serializer.serialize(sortedLinks, jgen, provider);
}
/*
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.ContextualSerializer#createContextual(org.codehaus.jackson.map.SerializationConfig, org.codehaus.jackson.map.BeanProperty)
*/
@@ -134,8 +156,92 @@ public class Jackson1HalModule extends SimpleModule {
}
/**
* Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. Renders the {@link Link} as
* immediate object if we have a single one or as array if we have multiple ones.
* Custom {@link JsonSerializer} to render {@link Resource}-Lists in HAL compatible JSON. Renders the list as a Map.
*
* @author Alexander Baetz
* @author Oliver Gierke
*/
public static class HalResourcesSerializer extends ContainerSerializerBase<Collection<?>> implements
ContextualSerializer<Collection<?>> {
private final BeanProperty property;
private final RelProvider relProvider;
public HalResourcesSerializer() {
this(null);
}
/**
* Creates a new {@link HalLinkListSerializer}.
*/
public HalResourcesSerializer(RelProvider relProvider) {
this(null, relProvider);
}
public HalResourcesSerializer(BeanProperty property, RelProvider relProvider) {
super(Collection.class, false);
this.property = property;
this.relProvider = relProvider;
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.ser.std.SerializerBase#serialize(java.lang.Object, org.codehaus.jackson.JsonGenerator, org.codehaus.jackson.map.SerializerProvider)
*/
@Override
public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
// sort resources according to their types
Map<String, List<Object>> sortedLinks = new HashMap<String, List<Object>>();
for (Object resource : value) {
String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass());
if (sortedLinks.get(relation) == null) {
sortedLinks.put(relation, new ArrayList<Object>());
}
sortedLinks.get(relation).add(resource);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Resource.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null, null,
provider.findKeySerializer(keyType, null), new OptionalListSerializer(property));
serializer.serialize(sortedLinks, jgen, provider);
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.ContextualSerializer#createContextual(org.codehaus.jackson.map.SerializationConfig, org.codehaus.jackson.map.BeanProperty)
*/
@Override
public JsonSerializer<Collection<?>> createContextual(SerializationConfig config, BeanProperty property)
throws JsonMappingException {
return new HalResourcesSerializer(property, relProvider);
}
/*
* (non-Javadoc)
*
* @see org.codehaus.jackson.map.ser.std.ContainerSerializerBase#_withValueTypeSerializer(org.codehaus.jackson.map.TypeSerializer)
*/
@Override
public ContainerSerializerBase<?> _withValueTypeSerializer(TypeSerializer vts) {
return null;
}
}
/**
* Custom {@link JsonSerializer} to render Objects in HAL compatible JSON. Renders the Object as immediate object if
* we have a single one or as array if we have multiple ones.
*
* @author Alexander Baetz
* @author Oliver Gierke
@@ -200,4 +306,152 @@ public class Jackson1HalModule extends SimpleModule {
}
}
}
public static class HalLinkListDeserializer extends ContainerDeserializerBase<List<Link>> {
public HalLinkListDeserializer() {
super(List.class);
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.deser.std.ContainerDeserializerBase#getContentType()
*/
@Override
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.deser.std.ContainerDeserializerBase#getContentDeserializer()
*/
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.JsonDeserializer#deserialize(org.codehaus.jackson.JsonParser, org.codehaus.jackson.map.DeserializationContext)
*/
@Override
public List<Link> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
List<Link> result = new ArrayList<Link>();
// links is an object, so we parse till we find its end.
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
throw new JsonParseException("Expected relation name", jp.getCurrentLocation());
}
// save the relation in case the link does not contain it
String relation = jp.getText();
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {
while (!JsonToken.END_ARRAY.equals(jp.nextToken())) {
result.add(getDefaultedLink(jp, relation));
}
} else {
result.add(getDefaultedLink(jp, relation));
}
}
return result;
}
private Link getDefaultedLink(JsonParser parser, String relation) throws JsonProcessingException, IOException {
Link link = parser.readValueAs(Link.class);
return StringUtils.hasText(link.getRel()) ? link : new Link(link.getHref(), relation);
}
}
public static class HalResourcesDeserializer extends ContainerDeserializerBase<List<Object>> implements
ContextualDeserializer<List<Object>> {
private final JavaType contentType;
public HalResourcesDeserializer() {
this(List.class, null);
}
public HalResourcesDeserializer(JavaType vc) {
this(null, vc);
}
private HalResourcesDeserializer(Class<?> type, JavaType contentType) {
super(type);
this.contentType = contentType;
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.deser.std.ContainerDeserializerBase#getContentType()
*/
@Override
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.deser.std.ContainerDeserializerBase#getContentDeserializer()
*/
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.JsonDeserializer#deserialize(org.codehaus.jackson.JsonParser, org.codehaus.jackson.map.DeserializationContext)
*/
@Override
public List<Object> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
List<Object> result = new ArrayList<Object>();
JsonDeserializer<Object> deser = ctxt.getDeserializerProvider().findTypedValueDeserializer(ctxt.getConfig(),
contentType, null);
Object object;
// links is an object, so we parse till we find its end.
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
throw new JsonParseException("Expected relation name", jp.getCurrentLocation());
}
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {
while (!JsonToken.END_ARRAY.equals(jp.nextToken())) {
object = deser.deserialize(jp, ctxt);
result.add(object);
}
} else {
object = deser.deserialize(jp, ctxt);
result.add(object);
}
}
return result;
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.ContextualDeserializer#createContextual(org.codehaus.jackson.map.DeserializationConfig, org.codehaus.jackson.map.BeanProperty)
*/
@Override
public JsonDeserializer<List<Object>> createContextual(DeserializationConfig config, BeanProperty property)
throws JsonMappingException {
JavaType vc = property.getType().getContentType();
HalResourcesDeserializer des = new HalResourcesDeserializer(vc);
return des;
}
}
}

View File

@@ -17,24 +17,37 @@ package org.springframework.hateoas.hal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.module.SimpleSerializers;
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
@@ -50,13 +63,17 @@ public class Jackson2HalModule extends SimpleModule {
private static final long serialVersionUID = 7806951456457932384L;
@SuppressWarnings("deprecation")
public Jackson2HalModule() {
public Jackson2HalModule(RelProvider relProvider) {
super("json-hal-module", new Version(1, 0, 0, null));
super("json-hal-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas"));
setMixInAnnotation(Link.class, LinkMixin.class);
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
setMixInAnnotation(Resources.class, ResourcesMixin.class);
SimpleSerializers serializers = new SimpleSerializers();
serializers.addSerializer(new HalResourcesSerializer(relProvider));
setSerializers(serializers);
}
/**
@@ -81,7 +98,9 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator,
* com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
@@ -109,7 +128,9 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty)
*
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider,
* com.fasterxml.jackson.databind.BeanProperty)
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
@@ -119,6 +140,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType()
*/
@Override
@@ -128,6 +150,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
@@ -137,6 +160,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object)
*/
@Override
@@ -146,6 +170,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object)
*/
@Override
@@ -155,7 +180,9 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.
* TypeSerializer)
*/
@Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
@@ -163,6 +190,107 @@ public class Jackson2HalModule extends SimpleModule {
}
}
/**
* Custom {@link JsonSerializer} to render {@link Resource}-Lists in HAL compatible JSON. Renders the list as a Map.
*
* @author Alexander Baetz
* @author Oliver Gierke
*/
public static class HalResourcesSerializer extends ContainerSerializer<Collection<?>> implements ContextualSerializer {
private final BeanProperty property;
private final RelProvider relProvider;
/**
* Creates a new {@link HalLinkListSerializer}.
*/
public HalResourcesSerializer() {
this(null);
}
public HalResourcesSerializer(RelProvider relPorvider) {
this(null, relPorvider);
}
public HalResourcesSerializer(BeanProperty property, RelProvider relProvider) {
super(Collection.class, false);
this.property = property;
this.relProvider = relProvider;
}
/*
* (non-Javadoc)
*
* @see org.codehaus.jackson.map.ser.std.SerializerBase#serialize(java.lang.Object, org.codehaus.jackson.JsonGenerator,
* org.codehaus.jackson.map.SerializerProvider)
*/
@Override
public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
// sort resources according to their types
Map<String, List<Object>> sortedLinks = new HashMap<String, List<Object>>();
for (Object resource : value) {
// TODO: do something fancy to get the relation name
String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass());
if (sortedLinks.get(relation) == null) {
sortedLinks.put(relation, new ArrayList<Object>());
}
sortedLinks.get(relation).add(resource);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Resource.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property));
serializer.serialize(sortedLinks, jgen, provider);
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
return new HalResourcesSerializer(property, null);
}
@Override
public JavaType getContentType() {
// TODO Auto-generated method stub
return null;
}
@Override
public JsonSerializer<?> getContentSerializer() {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isEmpty(Collection<?> value) {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean hasSingleElement(Collection<?> value) {
// TODO Auto-generated method stub
return false;
}
@Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
// TODO Auto-generated method stub
return null;
}
}
/**
* Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON. Renders the {@link Link} as
* immediate object if we have a single one or as array if we have multiple ones.
@@ -193,7 +321,9 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.
* TypeSerializer)
*/
@Override
public ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
@@ -202,7 +332,9 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator,
* com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
@@ -238,6 +370,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
@@ -247,6 +380,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType()
*/
@Override
@@ -256,6 +390,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object)
*/
@Override
@@ -265,6 +400,7 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object)
*/
@Override
@@ -274,7 +410,9 @@ public class Jackson2HalModule extends SimpleModule {
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty)
*
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider,
* com.fasterxml.jackson.databind.BeanProperty)
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
@@ -282,4 +420,148 @@ public class Jackson2HalModule extends SimpleModule {
return new OptionalListJackson2Serializer(property);
}
}
public static class HalLinkListDeserializer extends ContainerDeserializerBase<List<Link>> {
private static final long serialVersionUID = 6420432361123210955L;
public HalLinkListDeserializer() {
super(List.class);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType()
*/
@Override
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer()
*/
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public List<Link> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
List<Link> result = new ArrayList<Link>();
String relation;
Link link;
// links is an object, so we parse till we find its end.
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
throw new JsonParseException("Expected relation name", jp.getCurrentLocation());
}
// save the relation in case the link does not contain it
relation = jp.getText();
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {
while (!JsonToken.END_ARRAY.equals(jp.nextToken())) {
link = jp.readValueAs(Link.class);
result.add(new Link(link.getHref(), relation));
}
} else {
link = jp.readValueAs(Link.class);
result.add(new Link(link.getHref(), relation));
}
}
return result;
}
}
public static class HalResourcesDeserializer extends ContainerDeserializerBase<List<Object>> implements
ContextualDeserializer {
private static final long serialVersionUID = 4755806754621032622L;
private JavaType contentType;
public HalResourcesDeserializer() {
this(List.class, null);
}
public HalResourcesDeserializer(JavaType vc) {
this(null, vc);
}
private HalResourcesDeserializer(Class<?> type, JavaType contentType) {
super(type);
this.contentType = contentType;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType()
*/
@Override
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer()
*/
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public List<Object> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
List<Object> result = new ArrayList<Object>();
JsonDeserializer<Object> deser = ctxt.findRootValueDeserializer(contentType);
Object object;
// links is an object, so we parse till we find its end.
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
throw new JsonParseException("Expected relation name", jp.getCurrentLocation());
}
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {
while (!JsonToken.END_ARRAY.equals(jp.nextToken())) {
object = deser.deserialize(jp, ctxt);
;
result.add(object);
}
} else {
object = deser.deserialize(jp, ctxt);
result.add(object);
}
}
return result;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
JavaType vc = property.getType().getContentType();
HalResourcesDeserializer des = new HalResourcesDeserializer(vc);
return des;
}
}
}

View File

@@ -29,6 +29,8 @@ abstract class ResourceSupportMixin extends ResourceSupport {
@org.codehaus.jackson.annotate.JsonProperty("_links")
@com.fasterxml.jackson.annotation.JsonProperty("_links")
@org.codehaus.jackson.map.annotate.JsonSerialize(include = org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson1HalModule.HalLinkListSerializer.class)
@org.codehaus.jackson.map.annotate.JsonDeserialize(using = org.springframework.hateoas.hal.Jackson1HalModule.HalLinkListDeserializer.class)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(include = com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer.class)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListDeserializer.class)
public abstract List<Link> getLinks();
}

View File

@@ -0,0 +1,21 @@
package org.springframework.hateoas.hal;
import java.util.Collection;
import javax.xml.bind.annotation.XmlElement;
import org.springframework.hateoas.Resources;
public abstract class ResourcesMixin<T> extends Resources<T> {
@Override
@XmlElement(name = "embedded")
@org.codehaus.jackson.annotate.JsonProperty("_embedded")
@com.fasterxml.jackson.annotation.JsonProperty("_embedded")
@org.codehaus.jackson.map.annotate.JsonSerialize(include = org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson1HalModule.HalResourcesSerializer.class)
@org.codehaus.jackson.map.annotate.JsonDeserialize(using = org.springframework.hateoas.hal.Jackson1HalModule.HalResourcesDeserializer.class)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(include = com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson2HalModule.HalResourcesSerializer.class)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = org.springframework.hateoas.hal.Jackson2HalModule.HalResourcesDeserializer.class)
public abstract Collection<T> getContent();
}

View File

@@ -18,11 +18,16 @@ package org.springframework.hateoas.hal;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.hateoas.AbstractMarshallingIntegrationTests;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
/**
* Integration tests for Jackson 1 based HAL integration.
@@ -34,12 +39,14 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe
static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"test\":{}},\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"test\":[{},{}]},\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}";
static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}";
static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}";
@Before
public void setUpModule() {
mapper.registerModule(new Jackson1HalModule());
mapper.registerModule(new Jackson1HalModule(null));
}
/**
@@ -54,6 +61,15 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe
assertThat(write(resourceSupport), is(SINGLE_LINK_REFERENCE));
}
@Test
public void deserializeSingleLink() throws Exception {
ResourceSupport expected = new ResourceSupport();
expected.add(new Link("localhost"));
assertThat(read(SINGLE_LINK_REFERENCE, ResourceSupport.class), is(expected));
}
/**
* @see #29
*/
@@ -66,4 +82,103 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe
assertThat(write(resourceSupport), is(LIST_LINK_REFERENCE));
}
@Test
public void deserializeMultipleLinks() throws Exception {
ResourceSupport expected = new ResourceSupport();
expected.add(new Link("localhost"));
expected.add(new Link("localhost2"));
assertThat(read(LIST_LINK_REFERENCE, ResourceSupport.class), is(expected));
}
@Test
public void rendersSimpleResourcesAsEmbedded() throws Exception {
List<String> content = new ArrayList<String>();
content.add("first");
content.add("second");
Resources<String> resources = new Resources<String>(content);
resources.add(new Link("localhost"));
assertThat(write(resources), is(SIMPLE_EMBEDDED_RESOURCE_REFERENCE));
}
@Test
public void deserializesSimpleResourcesAsEmbedded() throws Exception {
List<String> content = new ArrayList<String>();
content.add("first");
content.add("second");
Resources<String> expected = new Resources<String>(content);
expected.add(new Link("localhost"));
Resources<String> result = mapper.readValue(SIMPLE_EMBEDDED_RESOURCE_REFERENCE, mapper.getTypeFactory()
.constructParametricType(Resources.class, String.class));
assertThat(result, is(expected));
}
@Test
public void rendersSingleResourceResourcesAsEmbedded() throws Exception {
List<Resource<SimplePojo>> content = new ArrayList<Resource<SimplePojo>>();
content.add(new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")));
Resources<Resource<SimplePojo>> resources = new Resources<Resource<SimplePojo>>(content);
resources.add(new Link("localhost"));
assertThat(write(resources), is(SINGLE_EMBEDDED_RESOURCE_REFERENCE));
}
@Test
public void deserializesSingleResourceResourcesAsEmbedded() throws Exception {
List<Resource<SimplePojo>> content = new ArrayList<Resource<SimplePojo>>();
content.add(new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")));
Resources<Resource<SimplePojo>> expected = new Resources<Resource<SimplePojo>>(content);
expected.add(new Link("localhost"));
Resources<Resource<SimplePojo>> result = mapper.readValue(
SINGLE_EMBEDDED_RESOURCE_REFERENCE,
mapper.getTypeFactory().constructParametricType(Resources.class,
mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class)));
assertThat(result, is(expected));
}
@Test
public void rendersMultipleResourceResourcesAsEmbedded() throws Exception {
List<Resource<SimplePojo>> content = new ArrayList<Resource<SimplePojo>>();
content.add(new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")));
content.add(new Resource<SimplePojo>(new SimplePojo("test2", 2), new Link("localhost")));
Resources<Resource<SimplePojo>> resources = new Resources<Resource<SimplePojo>>(content);
resources.add(new Link("localhost"));
assertThat(write(resources), is(LIST_EMBEDDED_RESOURCE_REFERENCE));
}
@Test
public void deserializeMultipleResourceResourcesAsEmbedded() throws Exception {
List<Resource<SimplePojo>> content = new ArrayList<Resource<SimplePojo>>();
content.add(new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")));
content.add(new Resource<SimplePojo>(new SimplePojo("test2", 2), new Link("localhost")));
Resources<Resource<SimplePojo>> expected = new Resources<Resource<SimplePojo>>(content);
expected.add(new Link("localhost"));
Resources<Resource<SimplePojo>> result = mapper.readValue(
LIST_EMBEDDED_RESOURCE_REFERENCE,
mapper.getTypeFactory().constructParametricType(Resources.class,
mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class)));
assertThat(result, is(expected));
}
}

View File

@@ -18,11 +18,16 @@ package org.springframework.hateoas.hal;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTests;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
/**
* Integration tests for Jackson 2 HAL integration.
@@ -34,12 +39,14 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"test\":{}},\"_links\":{\"self\":[\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_embedded\":{\"test\":[{},{}]},\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";
static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}";
static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}";
static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"href\":\"localhost\"}}}]}}";
@Before
public void setUpModule() {
mapper.registerModule(new Jackson2HalModule());
mapper.registerModule(new Jackson2HalModule(null));
}
/**
@@ -54,6 +61,13 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
assertThat(write(resourceSupport), is(SINGLE_LINK_REFERENCE));
}
@Test
public void deserializeSingleLink() throws Exception {
ResourceSupport expected = new ResourceSupport();
expected.add(new Link("localhost"));
assertThat(read(SINGLE_LINK_REFERENCE, ResourceSupport.class), is(expected));
}
/**
* @see #29
*/
@@ -66,4 +80,106 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg
assertThat(write(resourceSupport), is(LIST_LINK_REFERENCE));
}
@Test
public void deserializeMultipleLinks() throws Exception {
ResourceSupport expected = new ResourceSupport();
expected.add(new Link("localhost"));
expected.add(new Link("localhost2"));
assertThat(read(LIST_LINK_REFERENCE, ResourceSupport.class), is(expected));
}
@Test
public void rendersSimpleResourcesAsEmbedded() throws Exception {
List<String> content = new ArrayList<String>();
content.add("first");
content.add("second");
Resources<String> resources = new Resources<String>(content);
resources.add(new Link("localhost"));
assertThat(write(resources), is(SIMPLE_EMBEDDED_RESOURCE_REFERENCE));
}
@Test
public void deserializesSimpleResourcesAsEmbedded() throws Exception {
List<String> content = new ArrayList<String>();
content.add("first");
content.add("second");
Resources<String> expected = new Resources<String>(content);
expected.add(new Link("localhost"));
Resources<String> result = mapper.readValue(SIMPLE_EMBEDDED_RESOURCE_REFERENCE, mapper.getTypeFactory()
.constructParametricType(Resources.class, String.class));
assertThat(result, is(expected));
}
@Test
public void rendersSingleResourceResourcesAsEmbedded() throws Exception {
List<Resource<SimplePojo>> content = new ArrayList<Resource<SimplePojo>>();
content.add(new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")));
Resources<Resource<SimplePojo>> resources = new Resources<Resource<SimplePojo>>(content);
resources.add(new Link("localhost"));
assertThat(write(resources), is(SINGLE_EMBEDDED_RESOURCE_REFERENCE));
}
@Test
public void deserializesSingleResourceResourcesAsEmbedded() throws Exception {
List<Resource<SimplePojo>> content = new ArrayList<Resource<SimplePojo>>();
content.add(new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")));
Resources<Resource<SimplePojo>> expected = new Resources<Resource<SimplePojo>>(content);
expected.add(new Link("localhost"));
Resources<Resource<SimplePojo>> result = mapper.readValue(
SINGLE_EMBEDDED_RESOURCE_REFERENCE,
mapper.getTypeFactory().constructParametricType(Resources.class,
mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class)));
assertThat(result, is(expected));
}
@Test
public void rendersMultipleResourceResourcesAsEmbedded() throws Exception {
List<Resource<SimplePojo>> content = new ArrayList<Resource<SimplePojo>>();
content.add(new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")));
content.add(new Resource<SimplePojo>(new SimplePojo("test2", 2), new Link("localhost")));
Resources<Resource<SimplePojo>> resources = new Resources<Resource<SimplePojo>>(content);
resources.add(new Link("localhost"));
assertThat(write(resources), is(LIST_EMBEDDED_RESOURCE_REFERENCE));
}
@Test
public void deserializesMultipleResourceResourcesAsEmbedded() throws Exception {
List<Resource<SimplePojo>> content = new ArrayList<Resource<SimplePojo>>();
content.add(new Resource<SimplePojo>(new SimplePojo("test1", 1), new Link("localhost")));
content.add(new Resource<SimplePojo>(new SimplePojo("test2", 2), new Link("localhost")));
Resources<Resource<SimplePojo>> expected = new Resources<Resource<SimplePojo>>(content);
expected.add(new Link("localhost"));
Resources<Resource<SimplePojo>> result = mapper.readValue(
LIST_EMBEDDED_RESOURCE_REFERENCE,
mapper.getTypeFactory().constructParametricType(Resources.class,
mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class)));
assertThat(result, is(expected));
}
}

View File

@@ -0,0 +1,66 @@
package org.springframework.hateoas.hal;
public class SimplePojo {
private String text;
private int number;
public SimplePojo() {
}
public SimplePojo(String text, int number) {
this.text = text;
this.number = number;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + number;
result = prime * result + ((text == null) ? 0 : text.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
SimplePojo other = (SimplePojo) obj;
if (number != other.number) {
return false;
}
if (text == null) {
if (other.text != null) {
return false;
}
} else if (!text.equals(other.text)) {
return false;
}
return true;
}
}