From 47b50266b847af1663c6e4e364da6e6d3ca61e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Ba=CC=88tz?= Date: Mon, 11 Feb 2013 17:13:27 +0100 Subject: [PATCH] #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. --- ...ermediaSupportBeanDefinitionRegistrar.java | 4 +- .../hateoas/hal/Jackson1HalModule.java | 266 +++++++++++++++- .../hateoas/hal/Jackson2HalModule.java | 300 +++++++++++++++++- .../hateoas/hal/ResourceSupportMixin.java | 2 + .../hateoas/hal/ResourcesMixin.java | 21 ++ .../hal/Jackson1HalIntegrationTest.java | 121 ++++++- .../hal/Jackson2HalIntegrationTest.java | 122 ++++++- .../hateoas/hal/SimplePojo.java | 66 ++++ 8 files changed, 879 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java create mode 100644 src/test/java/org/springframework/hateoas/hal/SimplePojo.java diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index 13f84417..f4bd302d 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -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)); } } } diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java index d1b97a93..74edd347 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java @@ -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> implements + ContextualSerializer> { + + 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> sortedLinks = new HashMap>(); + + for (Object resource : value) { + + String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass()); + + if (sortedLinks.get(relation) == null) { + sortedLinks.put(relation, new ArrayList()); + } + + 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> 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> { + + 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 getContentDeserializer() { + return null; + } + + /* + * (non-Javadoc) + * @see org.codehaus.jackson.map.JsonDeserializer#deserialize(org.codehaus.jackson.JsonParser, org.codehaus.jackson.map.DeserializationContext) + */ + @Override + public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, + JsonProcessingException { + + List result = new ArrayList(); + + // 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> implements + ContextualDeserializer> { + + 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 getContentDeserializer() { + return null; + } + + /* + * (non-Javadoc) + * @see org.codehaus.jackson.map.JsonDeserializer#deserialize(org.codehaus.jackson.JsonParser, org.codehaus.jackson.map.DeserializationContext) + */ + @Override + public List deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, + JsonProcessingException { + + List result = new ArrayList(); + JsonDeserializer 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> createContextual(DeserializationConfig config, BeanProperty property) + throws JsonMappingException { + + JavaType vc = property.getType().getContentType(); + HalResourcesDeserializer des = new HalResourcesDeserializer(vc); + return des; + } + } } diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index eb07aa6b..c1491ad0 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -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 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> 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> sortedLinks = new HashMap>(); + + 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()); + } + + 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> { + + 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 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 deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, + JsonProcessingException { + + List result = new ArrayList(); + 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> 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 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 deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, + JsonProcessingException { + + List result = new ArrayList(); + JsonDeserializer 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; + } + } } diff --git a/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java b/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java index c8bd99db..e57ca81b 100644 --- a/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java +++ b/src/main/java/org/springframework/hateoas/hal/ResourceSupportMixin.java @@ -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 getLinks(); } diff --git a/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java b/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java new file mode 100644 index 00000000..302b6c38 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/hal/ResourcesMixin.java @@ -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 extends Resources { + + @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 getContent(); + +} diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java index 00683f85..0832ccb7 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java @@ -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 content = new ArrayList(); + content.add("first"); + content.add("second"); + + Resources resources = new Resources(content); + resources.add(new Link("localhost")); + + assertThat(write(resources), is(SIMPLE_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void deserializesSimpleResourcesAsEmbedded() throws Exception { + + List content = new ArrayList(); + content.add("first"); + content.add("second"); + + Resources expected = new Resources(content); + expected.add(new Link("localhost")); + + Resources 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> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + + Resources> resources = new Resources>(content); + resources.add(new Link("localhost")); + + assertThat(write(resources), is(SINGLE_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void deserializesSingleResourceResourcesAsEmbedded() throws Exception { + + List> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + + Resources> expected = new Resources>(content); + expected.add(new Link("localhost")); + + Resources> 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> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); + + Resources> resources = new Resources>(content); + resources.add(new Link("localhost")); + + assertThat(write(resources), is(LIST_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void deserializeMultipleResourceResourcesAsEmbedded() throws Exception { + + List> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); + + Resources> expected = new Resources>(content); + expected.add(new Link("localhost")); + + Resources> result = mapper.readValue( + LIST_EMBEDDED_RESOURCE_REFERENCE, + mapper.getTypeFactory().constructParametricType(Resources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); + + assertThat(result, is(expected)); + } } diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index 4e1e9ada..bb27efff 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -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 content = new ArrayList(); + content.add("first"); + content.add("second"); + + Resources resources = new Resources(content); + resources.add(new Link("localhost")); + + assertThat(write(resources), is(SIMPLE_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void deserializesSimpleResourcesAsEmbedded() throws Exception { + + List content = new ArrayList(); + content.add("first"); + content.add("second"); + + Resources expected = new Resources(content); + expected.add(new Link("localhost")); + + Resources 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> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + + Resources> resources = new Resources>(content); + resources.add(new Link("localhost")); + + assertThat(write(resources), is(SINGLE_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void deserializesSingleResourceResourcesAsEmbedded() throws Exception { + + List> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + + Resources> expected = new Resources>(content); + expected.add(new Link("localhost")); + + Resources> 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> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); + + Resources> resources = new Resources>(content); + resources.add(new Link("localhost")); + + assertThat(write(resources), is(LIST_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void deserializesMultipleResourceResourcesAsEmbedded() throws Exception { + + List> content = new ArrayList>(); + content.add(new Resource(new SimplePojo("test1", 1), new Link("localhost"))); + content.add(new Resource(new SimplePojo("test2", 2), new Link("localhost"))); + + Resources> expected = new Resources>(content); + expected.add(new Link("localhost")); + + Resources> result = mapper.readValue( + LIST_EMBEDDED_RESOURCE_REFERENCE, + mapper.getTypeFactory().constructParametricType(Resources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimplePojo.class))); + + assertThat(result, is(expected)); + + } } diff --git a/src/test/java/org/springframework/hateoas/hal/SimplePojo.java b/src/test/java/org/springframework/hateoas/hal/SimplePojo.java new file mode 100644 index 00000000..16fe7c40 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/hal/SimplePojo.java @@ -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; + } + +}