#29 - Added support for HAL-style representation of Links.

Added Jackson 1 and 2 modules to render Link objects in a HAL-compliant way. Introduced custom Jackson mixins to avoid introducing new value objects for the representation model. 

The rendering can be activated by simply registering Jackson1HalModule or Jackson2HalModule with the Jackson ObjectMapper.
This commit is contained in:
Alexander Bätz
2012-11-19 10:56:50 +01:00
committed by Oliver Gierke
parent c7396c24a9
commit 7af38348a9
9 changed files with 698 additions and 16 deletions

View File

@@ -0,0 +1,203 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.hal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.BeanProperty;
import org.codehaus.jackson.map.ContextualSerializer;
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.module.SimpleModule;
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.ResourceSupport;
/**
* Jackson 1 module implementation to render {@link Link} and {@link ResourceSupport} instances in HAL compatible JSON.
*
* @author Alexander Baetz
* @author Oliver Gierke
*/
public class Jackson1HalModule extends SimpleModule {
/**
* Creates a new {@link Jackson1HalModule}.
*/
public Jackson1HalModule() {
super("json-hal-module", new Version(1, 0, 0, null));
setMixInAnnotation(Link.class, LinkMixin.class);
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
}
/**
* Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON.
*
* @author Alexander Baetz
* @author Oliver Gierke
*/
public static class HalLinkListSerializer extends ContainerSerializerBase<List<Link>> implements
ContextualSerializer<List<Link>> {
private final BeanProperty property;
/**
* Creates a new {@link HalLinkListSerializer}.
*/
public HalLinkListSerializer() {
this(null);
}
public HalLinkListSerializer(BeanProperty property) {
super(List.class, false);
this.property = property;
}
/*
* (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(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
// sort links according to their relation
Map<String, List<Link>> sortedLinks = new HashMap<String, List<Link>>();
for (Link link : value) {
if (sortedLinks.get(link.getRel()) == null) {
sortedLinks.put(link.getRel(), new ArrayList<Link>());
}
sortedLinks.get(link.getRel()).add(link);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Link.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<List<Link>> createContextual(SerializationConfig config, BeanProperty property)
throws JsonMappingException {
return new HalLinkListSerializer(property);
}
/*
* (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 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.
*
* @author Alexander Baetz
* @author Oliver Gierke
*/
public static class OptionalListSerializer extends ContainerSerializerBase<Object> {
private final BeanProperty property;
private JsonSerializer<Object> serializer;
public OptionalListSerializer() {
this(null);
}
public OptionalListSerializer(BeanProperty property) {
super(List.class, false);
this.property = property;
}
/*
* (non-Javadoc)
* @see org.codehaus.jackson.map.ser.std.ContainerSerializerBase#_withValueTypeSerializer(org.codehaus.jackson.map.TypeSerializer)
*/
@Override
public ContainerSerializerBase<?> _withValueTypeSerializer(TypeSerializer vts) {
throw new UnsupportedOperationException("Not implemented");
}
/*
* (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(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
List<?> list = (List<?>) value;
if (list.size() == 1) {
serializeContents(list.iterator(), jgen, provider);
return;
}
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
if (serializer == null) {
serializer = provider.findValueSerializer(elem.getClass(), property);
}
serializer.serialize(elem, jgen, provider);
}
}
}
}
}

View File

@@ -0,0 +1,285 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.hal;
import java.io.IOException;
import java.util.ArrayList;
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.ResourceSupport;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
/**
* Jackson 2 module implementation to render {@link Link} and {@link ResourceSupport} instances in HAL compatible JSON.
*
* @author Alexander Baetz
* @author Oliver Gierke
*/
public class Jackson2HalModule extends SimpleModule {
private static final long serialVersionUID = 7806951456457932384L;
@SuppressWarnings("deprecation")
public Jackson2HalModule() {
super("json-hal-module", new Version(1, 0, 0, null));
setMixInAnnotation(Link.class, LinkMixin.class);
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
}
/**
* Custom {@link JsonSerializer} to render Link instances in HAL compatible JSON.
*
* @author Alexander Baetz
* @author Oliver Gierke
*/
public static class HalLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer {
private final BeanProperty property;
public HalLinkListSerializer() {
this(null);
}
public HalLinkListSerializer(BeanProperty property) {
super(List.class, false);
this.property = property;
}
/*
* (non-Javadoc)
* @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,
JsonGenerationException {
// sort links according to their relation
Map<String, List<Link>> sortedLinks = new HashMap<String, List<Link>>();
for (Link link : value) {
if (sortedLinks.get(link.getRel()) == null) {
sortedLinks.put(link.getRel(), new ArrayList<Link>());
}
sortedLinks.get(link.getRel()).add(link);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Link.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);
}
/*
* (non-Javadoc)
* @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)
throws JsonMappingException {
return new HalLinkListSerializer(property);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType()
*/
@Override
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object)
*/
@Override
public boolean isEmpty(List<Link> value) {
return false;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object)
*/
@Override
public boolean hasSingleElement(List<Link> value) {
return false;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer)
*/
@Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
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.
*
* @author Alexander Baetz
* @author Oliver Gierke
*/
public static class OptionalListJackson2Serializer extends ContainerSerializer<Object> implements
ContextualSerializer {
private final BeanProperty property;
private JsonSerializer<Object> serializer;
public OptionalListJackson2Serializer() {
this(null);
}
/**
* Creates a new {@link OptionalListJackson2Serializer} using the given {@link BeanProperty}.
*
* @param property
*/
public OptionalListJackson2Serializer(BeanProperty property) {
super(List.class, false);
this.property = property;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer)
*/
@Override
public ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
throw new UnsupportedOperationException("not implemented");
}
/*
* (non-Javadoc)
* @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,
JsonGenerationException {
List<?> list = (List<?>) value;
if (list.size() == 1) {
serializeContents(list.iterator(), jgen, provider);
return;
}
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
if (serializer == null) {
serializer = provider.findValueSerializer(elem.getClass(), property);
}
serializer.serialize(elem, jgen, provider);
}
}
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
public JsonSerializer<?> getContentSerializer() {
return serializer;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType()
*/
@Override
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object)
*/
@Override
public boolean hasSingleElement(Object arg0) {
return false;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object)
*/
@Override
public boolean isEmpty(Object arg0) {
return false;
}
/*
* (non-Javadoc)
* @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)
throws JsonMappingException {
return new OptionalListJackson2Serializer(property);
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.hal;
import org.springframework.hateoas.Link;
class LinkMixin extends Link {
private static final long serialVersionUID = 4720588561299667409L;
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.hal;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
abstract class ResourceSupportMixin extends ResourceSupport {
@Override
@XmlElement(name = "link")
@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)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(include = com.fasterxml.jackson.databind.annotation.JsonSerialize.Inclusion.NON_EMPTY, using = org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer.class)
public abstract List<Link> getLinks();
}