From f5a644121b73c071f93c475746dc0191b232808a Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 14 Feb 2013 17:42:34 +0100 Subject: [PATCH 1/4] Added provider interface for relation types by entity type. --- .../springframework/hateoas/RelProvider.java | 26 ++++++++ .../hateoas/mvc/ControllerRelProvider.java | 60 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/main/java/org/springframework/hateoas/RelProvider.java create mode 100644 src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java diff --git a/src/main/java/org/springframework/hateoas/RelProvider.java b/src/main/java/org/springframework/hateoas/RelProvider.java new file mode 100644 index 000000000..a9bb32853 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/RelProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 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. + * 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; + +/** + * @author Oliver Gierke + */ +public interface RelProvider { + + String getRelForCollectionResource(Class type); + + String getRelForSingleResource(Class type); +} diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java b/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java new file mode 100644 index 000000000..1d18b58ac --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java @@ -0,0 +1,60 @@ +/* + * Copyright 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. + * 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.mvc; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.hateoas.ExposesResourceFor; +import org.springframework.hateoas.RelProvider; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Oliver Gierke + */ +public class ControllerRelProvider implements RelProvider { + + private final Class entityType; + private final String collectionResourceRel; + private final String singleResourceRel; + + public ControllerRelProvider(Class controller) { + + ExposesResourceFor annotation = AnnotationUtils.findAnnotation(controller, ExposesResourceFor.class); + Assert.notNull(annotation); + + this.entityType = annotation.value(); + this.singleResourceRel = StringUtils.uncapitalize(entityType.getSimpleName()); + this.collectionResourceRel = singleResourceRel + "List"; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class) + */ + @Override + public String getRelForCollectionResource(Class type) { + return collectionResourceRel; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class) + */ + @Override + public String getRelForSingleResource(Class type) { + return singleResourceRel; + } +} From 831d320e43dc82f93ffc193dd1ef882b070d24e7 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 2/4] collections as _embedded according to hal collections are supposed to be serialized as _embedded with a topical relation (see https://groups.google.com/forum/?fromgroups=#!topic/hal-discuss/bCzydTlHB3M ) fixed formating fixed contextual implementation initial version of deserializers final cleanup before deserializers added new link deserializer need to find a way to handle "optional" relation property in hal working towards _embedded deserialization deserializers done some more tests and they should be ready final cleanup and tests fix formating aggain --- .gitignore | 1 + .../hateoas/hal/Jackson1HalModule.java | 264 +++++++++++++++++- .../hateoas/hal/Jackson2HalModule.java | 252 ++++++++++++++++- .../hateoas/hal/ResourceSupportMixin.java | 2 + .../hateoas/hal/ResourcesMixin.java | 21 ++ .../hal/Jackson1HalIntegrationTest.java | 125 ++++++++- .../hal/Jackson2HalIntegrationTest.java | 128 ++++++++- .../hateoas/hal/SimplePojo.java | 66 +++++ 8 files changed, 836 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/.gitignore b/.gitignore index 5be812dbb..8c84b71be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target/ +test-output/ .settings/ .project .classpath \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java index d1b97a939..0cc178778 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 @@ 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 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. @@ -48,19 +63,30 @@ */ public class Jackson1HalModule extends SimpleModule { + private final RelProvider relProvider; + /** * 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()); + + setSerializers(serializers); + + this.relProvider = relProvider; } /** - * 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 +139,7 @@ public void serialize(List value, JsonGenerator jgen, SerializerProvider p 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 +160,86 @@ public ContainerSerializerBase _withValueTypeSerializer(TypeSerializer vts) { } /** - * 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 class HalResourcesSerializer extends ContainerSerializerBase> implements + ContextualSerializer> { + + private final BeanProperty property; + + /** + * Creates a new {@link HalLinkListSerializer}. + */ + public HalResourcesSerializer() { + this(null); + } + + public HalResourcesSerializer(BeanProperty property) { + super(Collection.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(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 = property == null || relProvider == null ? "content" : relProvider + .getRelForSingleResource(property.getType().getRawClass()); + + 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); + } + + /* + * (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 +304,152 @@ private void serializeContents(Iterator value, JsonGenerator jgen, Serializer } } } + + 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 eb07aa6b6..fdcf60d01 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -17,22 +17,33 @@ 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.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.ser.ContainerSerializer; @@ -57,6 +68,7 @@ public Jackson2HalModule() { setMixInAnnotation(Link.class, LinkMixin.class); setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class); + setMixInAnnotation(Resources.class, ResourcesMixin.class); } /** @@ -81,7 +93,9 @@ public HalLinkListSerializer(BeanProperty 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) + * + * @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 +123,9 @@ public void serialize(List value, JsonGenerator jgen, SerializerProvider p /* * (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 +135,7 @@ public JsonSerializer createContextual(SerializerProvider provider, BeanPrope /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override @@ -128,6 +145,7 @@ public JavaType getContentType() { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override @@ -137,6 +155,7 @@ public JsonSerializer getContentSerializer() { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object) */ @Override @@ -146,6 +165,7 @@ public boolean isEmpty(List value) { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) */ @Override @@ -155,7 +175,9 @@ public boolean hasSingleElement(List value) { /* * (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 +185,100 @@ protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { } } + /** + * 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; + + /** + * Creates a new {@link HalLinkListSerializer}. + */ + public HalResourcesSerializer() { + this(null); + } + + public HalResourcesSerializer(BeanProperty property) { + super(Collection.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(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 = "content"; + 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); + } + + @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 +309,9 @@ public OptionalListJackson2Serializer(BeanProperty property) { /* * (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 +320,9 @@ public ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { /* * (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 +358,7 @@ private void serializeContents(Iterator value, JsonGenerator jgen, Serializer /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override @@ -247,6 +368,7 @@ public JsonSerializer getContentSerializer() { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override @@ -256,6 +378,7 @@ public JavaType getContentType() { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) */ @Override @@ -265,6 +388,7 @@ public boolean hasSingleElement(Object arg0) { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object) */ @Override @@ -274,7 +398,9 @@ public boolean isEmpty(Object arg0) { /* * (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 +408,118 @@ public JsonSerializer createContextual(SerializerProvider provider, BeanPrope return new OptionalListJackson2Serializer(property); } } + + public static class HalLinkListDeserializer extends ContainerDeserializerBase> { + + public HalLinkListDeserializer() { + super(List.class); + } + + @Override + public JavaType getContentType() { + return null; + } + + @Override + public JsonDeserializer getContentDeserializer() { + return null; + } + + @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(link); + } + } else { + link = jp.readValueAs(Link.class); + result.add(link); + } + } + + return result; + } + } + + public static class HalResourcesDeserializer extends ContainerDeserializerBase> implements + ContextualDeserializer { + + private JavaType contentType; + + public HalResourcesDeserializer() { + super(List.class); + } + + public HalResourcesDeserializer(JavaType vc) { + super(null); + this.contentType = vc; + } + + @Override + public JavaType getContentType() { + return null; + } + + @Override + public JsonDeserializer getContentDeserializer() { + return null; + } + + @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(); + + // if (INSTANCES.containsKey(vc)) { + // return INSTANCES.get(vc); + // } + HalResourcesDeserializer des = new HalResourcesDeserializer(vc); + // INSTANCES.put(vc, des); + 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 c8bd99db3..e57ca81b1 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 000000000..302b6c388 --- /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 00683f85d..b926cfbdd 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 @@ 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. @@ -32,14 +37,16 @@ */ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTests { - 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 SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}"; + static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"rel\":\"self\",\"href\":\"localhost\"},{\"rel\":\"self\",\"href\":\"localhost2\"}]}}"; + + static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}"; + static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}}}"; + static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}]}}"; @Before public void setUpModule() { - mapper.registerModule(new Jackson1HalModule()); + mapper.registerModule(new Jackson1HalModule(null)); } /** @@ -54,6 +61,15 @@ public void rendersSingleLinkAsObject() throws Exception { 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 void rendersMultipleLinkAsArray() throws Exception { 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 4e1e9adae..5ebd53b7f 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -15,14 +15,19 @@ */ package org.springframework.hateoas.hal; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +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. @@ -32,10 +37,12 @@ */ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationTests { - 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 SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}"; + static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"rel\":\"self\",\"href\":\"localhost\"},{\"rel\":\"self\",\"href\":\"localhost2\"}]}}"; + + static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}"; + static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}}}"; + static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}]}}"; @Before public void setUpModule() { @@ -54,6 +61,13 @@ public void rendersSingleLinkAsObject() throws Exception { 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 void rendersMultipleLinkAsArray() throws Exception { 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 000000000..16fe7c407 --- /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; + } + +} From 8c71a04058da2f5546fac093de5174aaa4b68f47 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 12 Apr 2013 11:39:09 +0200 Subject: [PATCH 3/4] Got codebase build and pass tests again. --- ...ermediaSupportBeanDefinitionRegistrar.java | 4 +- .../hateoas/hal/Jackson1HalModule.java | 26 +++--- .../hateoas/hal/Jackson2HalModule.java | 80 ++++++++++++++----- .../hal/Jackson1HalIntegrationTest.java | 12 +-- .../hal/Jackson2HalIntegrationTest.java | 16 ++-- 5 files changed, 91 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index 13f844173..f4bd302d6 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 @@ private void registerModule(List> converters) { } private void registerModule(Object objectMapper) { - ((ObjectMapper) objectMapper).registerModule(new Jackson2HalModule()); + ((ObjectMapper) objectMapper).registerModule(new Jackson2HalModule(null)); } } @@ -217,7 +217,7 @@ private void registerModule(List> converters) { } 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 0cc178778..74edd347b 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java @@ -63,8 +63,6 @@ */ public class Jackson1HalModule extends SimpleModule { - private final RelProvider relProvider; - /** * Creates a new {@link Jackson1HalModule}. */ @@ -77,11 +75,9 @@ public Jackson1HalModule(RelProvider relProvider) { setMixInAnnotation(Resources.class, ResourcesMixin.class); SimpleSerializers serializers = new SimpleSerializers(); - serializers.addSerializer(new HalResourcesSerializer()); + serializers.addSerializer(new HalResourcesSerializer(relProvider)); setSerializers(serializers); - - this.relProvider = relProvider; } /** @@ -165,21 +161,28 @@ public ContainerSerializerBase _withValueTypeSerializer(TypeSerializer vts) { * @author Alexander Baetz * @author Oliver Gierke */ - public class HalResourcesSerializer extends ContainerSerializerBase> implements + 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() { - this(null); + public HalResourcesSerializer(RelProvider relProvider) { + this(null, relProvider); } - public HalResourcesSerializer(BeanProperty property) { + public HalResourcesSerializer(BeanProperty property, RelProvider relProvider) { + super(Collection.class, false); this.property = property; + this.relProvider = relProvider; } /* @@ -195,8 +198,7 @@ public void serialize(Collection value, JsonGenerator jgen, SerializerProvide for (Object resource : value) { - String relation = property == null || relProvider == null ? "content" : relProvider - .getRelForSingleResource(property.getType().getRawClass()); + String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass()); if (sortedLinks.get(relation) == null) { sortedLinks.put(relation, new ArrayList()); @@ -223,7 +225,7 @@ public void serialize(Collection value, JsonGenerator jgen, SerializerProvide @Override public JsonSerializer> createContextual(SerializationConfig config, BeanProperty property) throws JsonMappingException { - return new HalResourcesSerializer(property); + return new HalResourcesSerializer(property, relProvider); } /* diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index fdcf60d01..c1491ad07 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -24,6 +24,7 @@ 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; @@ -46,6 +47,7 @@ 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; @@ -61,14 +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); } /** @@ -194,6 +199,7 @@ protected ContainerSerializer _withValueTypeSerializer(TypeSerializer vts) { public static class HalResourcesSerializer extends ContainerSerializer> implements ContextualSerializer { private final BeanProperty property; + private final RelProvider relProvider; /** * Creates a new {@link HalLinkListSerializer}. @@ -202,9 +208,15 @@ public HalResourcesSerializer() { this(null); } - public HalResourcesSerializer(BeanProperty property) { + public HalResourcesSerializer(RelProvider relPorvider) { + this(null, relPorvider); + } + + public HalResourcesSerializer(BeanProperty property, RelProvider relProvider) { + super(Collection.class, false); this.property = property; + this.relProvider = relProvider; } /* @@ -223,7 +235,7 @@ public void serialize(Collection value, JsonGenerator jgen, SerializerProvide for (Object resource : value) { // TODO: do something fancy to get the relation name - String relation = "content"; + String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass()); if (sortedLinks.get(relation) == null) { sortedLinks.put(relation, new ArrayList()); } @@ -245,7 +257,7 @@ public void serialize(Collection value, JsonGenerator jgen, SerializerProvide @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - return new HalResourcesSerializer(property); + return new HalResourcesSerializer(property, null); } @Override @@ -411,27 +423,42 @@ public JsonSerializer createContextual(SerializerProvider provider, BeanPrope 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(); + 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())) { @@ -444,11 +471,11 @@ public List deserialize(JsonParser jp, DeserializationContext ctxt) throws if (JsonToken.START_ARRAY.equals(jp.nextToken())) { while (!JsonToken.END_ARRAY.equals(jp.nextToken())) { link = jp.readValueAs(Link.class); - result.add(link); + result.add(new Link(link.getHref(), relation)); } } else { link = jp.readValueAs(Link.class); - result.add(link); + result.add(new Link(link.getHref(), relation)); } } @@ -459,35 +486,54 @@ public List deserialize(JsonParser jp, DeserializationContext ctxt) throws public static class HalResourcesDeserializer extends ContainerDeserializerBase> implements ContextualDeserializer { + private static final long serialVersionUID = 4755806754621032622L; + private JavaType contentType; public HalResourcesDeserializer() { - super(List.class); + this(List.class, null); } public HalResourcesDeserializer(JavaType vc) { - super(null); - this.contentType = 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(); + 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())) { @@ -512,13 +558,9 @@ public List deserialize(JsonParser jp, DeserializationContext ctxt) thro @Override public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { - JavaType vc = property.getType().getContentType(); - // if (INSTANCES.containsKey(vc)) { - // return INSTANCES.get(vc); - // } + JavaType vc = property.getType().getContentType(); HalResourcesDeserializer des = new HalResourcesDeserializer(vc); - // INSTANCES.put(vc, des); return des; } } diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java index b926cfbdd..0832ccb7b 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java @@ -37,12 +37,12 @@ */ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTests { - static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}"; - static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"rel\":\"self\",\"href\":\"localhost\"},{\"rel\":\"self\",\"href\":\"localhost2\"}]}}"; - - static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}"; - static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}}}"; - static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}]}}"; + 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 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() { diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java index 5ebd53b7f..bb27efffd 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -15,8 +15,8 @@ */ package org.springframework.hateoas.hal; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import java.util.ArrayList; import java.util.List; @@ -37,16 +37,16 @@ */ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationTests { - static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}"; - static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"rel\":\"self\",\"href\":\"localhost\"},{\"rel\":\"self\",\"href\":\"localhost2\"}]}}"; + 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 SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}"; - static final String SINGLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}}}"; - static final String LIST_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}},\"_embedded\":{\"content\":[{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}},{\"text\":\"test2\",\"number\":2,\"_links\":{\"self\":{\"rel\":\"self\",\"href\":\"localhost\"}}}]}}"; + 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)); } /** From 88a2d1e53bb390e28e6495e599e805f19e12c10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20B=C3=A4tz?= Date: Mon, 15 Apr 2013 17:31:08 +0200 Subject: [PATCH 4/4] readded parts of hal support and configuration - changed rel provider to handle objects, not classes. otherwise the wrapper classes (Resource...) could not be easyly handled - added new annotation based relprovider - changed way to configure a rel provider. serializers.addserializer did not work Tests - tests for annotationrelprovider --- .../springframework/hateoas/RelProvider.java | 4 +- ...ermediaSupportBeanDefinitionRegistrar.java | 19 ++--- .../hateoas/hal/AnnotationRelProvider.java | 61 ++++++++++++++ .../hateoas/hal/HalRelation.java | 18 +++++ .../hateoas/hal/Jackson1HalModule.java | 79 ++++++++++++++++-- .../hateoas/hal/Jackson2HalModule.java | 81 ++++++++++++++++--- .../hateoas/mvc/ControllerRelProvider.java | 4 +- .../hal/Jackson1HalIntegrationTest.java | 41 +++++++++- .../hal/Jackson2HalIntegrationTest.java | 41 +++++++++- .../hateoas/hal/SimpleAnnotatedPojo.java | 14 ++++ 10 files changed, 325 insertions(+), 37 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/hal/AnnotationRelProvider.java create mode 100644 src/main/java/org/springframework/hateoas/hal/HalRelation.java create mode 100644 src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java diff --git a/src/main/java/org/springframework/hateoas/RelProvider.java b/src/main/java/org/springframework/hateoas/RelProvider.java index a9bb32853..d222a61e0 100644 --- a/src/main/java/org/springframework/hateoas/RelProvider.java +++ b/src/main/java/org/springframework/hateoas/RelProvider.java @@ -20,7 +20,7 @@ */ public interface RelProvider { - String getRelForCollectionResource(Class type); + String getRelForCollectionResource(Object type); - String getRelForSingleResource(Class type); + String getRelForSingleResource(Object type); } diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index f4bd302d6..da38730c4 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -15,7 +15,8 @@ */ package org.springframework.hateoas.config; -import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*; +import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.registerBeanDefinition; +import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.registerWithGeneratedName; import java.util.List; import java.util.Map; @@ -103,12 +104,12 @@ private AbstractBeanDefinition getLinkDiscovererBeanDefinition(HypermediaType ty AbstractBeanDefinition definition; switch (type) { - case HAL: - definition = new RootBeanDefinition(HalLinkDiscoverer.class); - break; - case DEFAULT: - default: - definition = new RootBeanDefinition(DefaultLinkDiscoverer.class); + case HAL: + definition = new RootBeanDefinition(HalLinkDiscoverer.class); + break; + case DEFAULT: + default: + definition = new RootBeanDefinition(DefaultLinkDiscoverer.class); } definition.setSource(this); @@ -164,7 +165,7 @@ private void registerModule(List> converters) { } private void registerModule(Object objectMapper) { - ((ObjectMapper) objectMapper).registerModule(new Jackson2HalModule(null)); + ((ObjectMapper) objectMapper).registerModule(new Jackson2HalModule()); } } @@ -217,7 +218,7 @@ private void registerModule(List> converters) { } private void registerModule(Object mapper) { - ((org.codehaus.jackson.map.ObjectMapper) mapper).registerModule(new Jackson1HalModule(null)); + ((org.codehaus.jackson.map.ObjectMapper) mapper).registerModule(new Jackson1HalModule()); } } } diff --git a/src/main/java/org/springframework/hateoas/hal/AnnotationRelProvider.java b/src/main/java/org/springframework/hateoas/hal/AnnotationRelProvider.java new file mode 100644 index 000000000..fdab68133 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/hal/AnnotationRelProvider.java @@ -0,0 +1,61 @@ +/* + * Copyright 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. + * 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.RelProvider; +import org.springframework.hateoas.Resource; + +/** + * @author Oliver Gierke + */ +public class AnnotationRelProvider implements RelProvider { + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Object) + */ + @Override + public String getRelForCollectionResource(Object resource) { + // check for hateoas wrapper type + if (Resource.class.isInstance(resource)) { + resource = ((Resource) resource).getContent(); + } + + HalRelation annotation = resource.getClass().getAnnotation(HalRelation.class); + if (annotation == null || HalRelation.NO_RELATION.equals(annotation.collectionRelation())) { + return null; + } + return annotation.value(); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Object) + */ + @Override + public String getRelForSingleResource(Object resource) { + // check for hateoas wrapper type + if (Resource.class.isInstance(resource)) { + resource = ((Resource) resource).getContent(); + } + + HalRelation annotation = resource.getClass().getAnnotation(HalRelation.class); + if (annotation == null || HalRelation.NO_RELATION.equals(annotation.value())) { + return null; + } + return annotation.value(); + } +} diff --git a/src/main/java/org/springframework/hateoas/hal/HalRelation.java b/src/main/java/org/springframework/hateoas/hal/HalRelation.java new file mode 100644 index 000000000..ff3a310b2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/hal/HalRelation.java @@ -0,0 +1,18 @@ +package org.springframework.hateoas.hal; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface HalRelation { + + public static final String NO_RELATION = ""; + + String value() default NO_RELATION; + + String collectionRelation() default NO_RELATION; + +} \ No newline at end of file diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java index 74edd347b..47595fdeb 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java @@ -35,15 +35,20 @@ import org.codehaus.jackson.map.ContextualSerializer; import org.codehaus.jackson.map.DeserializationConfig; import org.codehaus.jackson.map.DeserializationContext; +import org.codehaus.jackson.map.HandlerInstantiator; import org.codehaus.jackson.map.JsonDeserializer; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.JsonSerializer; +import org.codehaus.jackson.map.KeyDeserializer; +import org.codehaus.jackson.map.MapperConfig; 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.introspect.Annotated; +import org.codehaus.jackson.map.jsontype.TypeIdResolver; +import org.codehaus.jackson.map.jsontype.TypeResolverBuilder; 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; @@ -66,18 +71,13 @@ public class Jackson1HalModule extends SimpleModule { /** * Creates a new {@link Jackson1HalModule}. */ - public Jackson1HalModule(RelProvider relProvider) { + public Jackson1HalModule() { 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); } /** @@ -198,7 +198,10 @@ public void serialize(Collection value, JsonGenerator jgen, SerializerProvide for (Object resource : value) { - String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass()); + String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(resource); + if (relation == null) { + relation = "content"; + } if (sortedLinks.get(relation) == null) { sortedLinks.put(relation, new ArrayList()); @@ -454,4 +457,64 @@ public JsonDeserializer> createContextual(DeserializationConfig con return des; } } + + public static class HalHandlerInstantiator extends HandlerInstantiator { + + private Map instanceMap = new HashMap(); + + public void setInstanceMap(Map instanceMap) { + this.instanceMap = instanceMap; + } + + public void setRelationResolver(RelProvider provider) { + instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(null, provider)); + } + + private Object findInstance(Class type, boolean createInstance) { + Object result = instanceMap.get(type); + if (null == result && createInstance) { + try { + result = type.newInstance(); + } catch (InstantiationException e) { + return new RuntimeException(e); + } catch (IllegalAccessException e) { + return new RuntimeException(e); + } + } + return result; + } + + @Override + public JsonDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, + Class> deserClass) { + return (JsonDeserializer) findInstance(deserClass, false); + } + + @Override + public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, + Class keyDeserClass) { + return (KeyDeserializer) findInstance(keyDeserClass, false); + } + + @Override + public JsonSerializer serializerInstance(SerializationConfig config, Annotated annotated, + Class> serClass) { + // there is a know bug in jackson that will not create a serializer instance if the handler instantiator returns + // null! + return (JsonSerializer) findInstance(serClass, true); + } + + @Override + public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, + Class> builderClass) { + return (TypeResolverBuilder) findInstance(builderClass, false); + } + + @Override + public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated annotated, + Class resolverClass) { + return (TypeIdResolver) findInstance(resolverClass, false); + } + + } } diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index c1491ad07..6b0a61e60 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -37,17 +37,24 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationConfig; 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.KeyDeserializer; +import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; +import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; 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; @@ -63,17 +70,13 @@ public class Jackson2HalModule extends SimpleModule { private static final long serialVersionUID = 7806951456457932384L; - public Jackson2HalModule(RelProvider relProvider) { + public Jackson2HalModule() { 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); } /** @@ -234,12 +237,14 @@ public void serialize(Collection value, JsonGenerator jgen, SerializerProvide for (Object resource : value) { - // TODO: do something fancy to get the relation name - String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass()); + String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(resource); + if (relation == null) { + relation = "content"; + } + if (sortedLinks.get(relation) == null) { sortedLinks.put(relation, new ArrayList()); } - sortedLinks.get(relation).add(resource); } @@ -257,7 +262,7 @@ public void serialize(Collection value, JsonGenerator jgen, SerializerProvide @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - return new HalResourcesSerializer(property, null); + return new HalResourcesSerializer(property, relProvider); } @Override @@ -564,4 +569,60 @@ public JsonDeserializer createContextual(DeserializationContext ctxt, BeanPro return des; } } + + public static class HalHandlerInstantiator extends HandlerInstantiator { + + private Map instanceMap = new HashMap(); + + public void setInstanceMap(Map instanceMap) { + this.instanceMap = instanceMap; + } + + public void setRelationResolver(RelProvider resolver) { + instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(null, resolver)); + } + + private Object findInstance(Class type) { + Object result = instanceMap.get(type); + if (null == result) { + try { + result = type.newInstance(); + } catch (InstantiationException e) { + return new RuntimeException(e); + } catch (IllegalAccessException e) { + return new RuntimeException(e); + } + } + return result; + } + + @Override + public JsonDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, + Class deserClass) { + return (JsonDeserializer) findInstance(deserClass); + } + + @Override + public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, + Class keyDeserClass) { + return (KeyDeserializer) findInstance(keyDeserClass); + } + + @Override + public JsonSerializer serializerInstance(SerializationConfig config, Annotated annotated, Class serClass) { + return (JsonSerializer) findInstance(serClass); + } + + @Override + public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, + Class builderClass) { + return (TypeResolverBuilder) findInstance(builderClass); + } + + @Override + public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated annotated, Class resolverClass) { + return (TypeIdResolver) findInstance(resolverClass); + } + + } } diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java b/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java index 1d18b58ac..84119efde 100644 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java @@ -45,7 +45,7 @@ public ControllerRelProvider(Class controller) { * @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class) */ @Override - public String getRelForCollectionResource(Class type) { + public String getRelForCollectionResource(Object resource) { return collectionResourceRel; } @@ -54,7 +54,7 @@ public String getRelForCollectionResource(Class type) { * @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class) */ @Override - public String getRelForSingleResource(Class type) { + public String getRelForSingleResource(Object resource) { return singleResourceRel; } } diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java index 0832ccb7b..7c2068918 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java @@ -15,8 +15,8 @@ */ package org.springframework.hateoas.hal; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.List; @@ -44,9 +44,14 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe 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\"}}}]}}"; + static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojo\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}"; + @Before public void setUpModule() { - mapper.registerModule(new Jackson1HalModule(null)); + mapper.registerModule(new Jackson1HalModule()); + Jackson1HalModule.HalHandlerInstantiator hi = new Jackson1HalModule.HalHandlerInstantiator(); + hi.setRelationResolver(new AnnotationRelProvider()); + mapper.setHandlerInstantiator(hi); } /** @@ -181,4 +186,34 @@ public void deserializeMultipleResourceResourcesAsEmbedded() throws Exception { assertThat(result, is(expected)); } + + @Test + public void rendersAnnotatedResourceResourcesAsEmbedded() throws Exception { + + List> content = new ArrayList>(); + content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); + + Resources> resources = new Resources>(content); + resources.add(new Link("localhost")); + + assertThat(write(resources), is(ANNOTATED_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void deserializesAnnotatedResourceResourcesAsEmbedded() throws Exception { + + List> content = new ArrayList>(); + content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); + + Resources> expected = new Resources>(content); + expected.add(new Link("localhost")); + + Resources> result = mapper.readValue( + ANNOTATED_EMBEDDED_RESOURCE_REFERENCE, + mapper.getTypeFactory().constructParametricType(Resources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.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 bb27efffd..5826fc5ae 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -15,8 +15,8 @@ */ package org.springframework.hateoas.hal; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.List; @@ -28,6 +28,7 @@ import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator; /** * Integration tests for Jackson 2 HAL integration. @@ -44,9 +45,14 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg 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\"}}}]}}"; + static final String ANNOTATED_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"pojo\":{\"text\":\"test1\",\"number\":1,\"_links\":{\"self\":{\"href\":\"localhost\"}}}}}"; + @Before public void setUpModule() { - mapper.registerModule(new Jackson2HalModule(null)); + mapper.registerModule(new Jackson2HalModule()); + HalHandlerInstantiator hi = new Jackson2HalModule.HalHandlerInstantiator(); + hi.setRelationResolver(new AnnotationRelProvider()); + mapper.setHandlerInstantiator(hi); } /** @@ -182,4 +188,33 @@ public void deserializesMultipleResourceResourcesAsEmbedded() throws Exception { assertThat(result, is(expected)); } + + @Test + public void rendersAnnotatedResourceResourcesAsEmbedded() throws Exception { + + List> content = new ArrayList>(); + content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); + + Resources> resources = new Resources>(content); + resources.add(new Link("localhost")); + + assertThat(write(resources), is(ANNOTATED_EMBEDDED_RESOURCE_REFERENCE)); + } + + @Test + public void deserializesAnnotatedResourceResourcesAsEmbedded() throws Exception { + + List> content = new ArrayList>(); + content.add(new Resource(new SimpleAnnotatedPojo("test1", 1), new Link("localhost"))); + + Resources> expected = new Resources>(content); + expected.add(new Link("localhost")); + + Resources> result = mapper.readValue( + ANNOTATED_EMBEDDED_RESOURCE_REFERENCE, + mapper.getTypeFactory().constructParametricType(Resources.class, + mapper.getTypeFactory().constructParametricType(Resource.class, SimpleAnnotatedPojo.class))); + + assertThat(result, is(expected)); + } } diff --git a/src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java b/src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java new file mode 100644 index 000000000..a6b2dc647 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java @@ -0,0 +1,14 @@ +package org.springframework.hateoas.hal; + +@HalRelation(value = "pojo", collectionRelation = "pojo") +public class SimpleAnnotatedPojo extends SimplePojo { + + public SimpleAnnotatedPojo() { + } + + public SimpleAnnotatedPojo(String text, int number) { + setText(text); + setNumber(number); + } + +}