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/RelProvider.java b/src/main/java/org/springframework/hateoas/RelProvider.java new file mode 100644 index 000000000..d222a61e0 --- /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(Object 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 13f844173..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); 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 d1b97a939..47595fdeb 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,40 @@ 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.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.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. @@ -57,10 +77,12 @@ public Jackson1HalModule() { setMixInAnnotation(Link.class, LinkMixin.class); setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class); + setMixInAnnotation(Resources.class, ResourcesMixin.class); } /** - * 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 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 +156,95 @@ 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 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(resource); + if (relation == null) { + 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, 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 +309,212 @@ 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; + } + } + + 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 eb07aa6b6..6b0a61e60 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -17,22 +17,42 @@ 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.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.ser.ContainerSerializer; @@ -50,13 +70,13 @@ public class Jackson2HalModule extends SimpleModule { private static final long serialVersionUID = 7806951456457932384L; - @SuppressWarnings("deprecation") public Jackson2HalModule() { - super("json-hal-module", new Version(1, 0, 0, null)); + 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); } /** @@ -81,7 +101,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 +131,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 +143,7 @@ public JsonSerializer createContextual(SerializerProvider provider, BeanPrope /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override @@ -128,6 +153,7 @@ public JavaType getContentType() { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override @@ -137,6 +163,7 @@ public JsonSerializer getContentSerializer() { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object) */ @Override @@ -146,6 +173,7 @@ public boolean isEmpty(List value) { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) */ @Override @@ -155,7 +183,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 +193,109 @@ 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; + 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) { + + 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); + } + + 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, relProvider); + } + + @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 +326,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 +337,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 +375,7 @@ private void serializeContents(Iterator value, JsonGenerator jgen, Serializer /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer() */ @Override @@ -247,6 +385,7 @@ public JsonSerializer getContentSerializer() { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType() */ @Override @@ -256,6 +395,7 @@ public JavaType getContentType() { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object) */ @Override @@ -265,6 +405,7 @@ public boolean hasSingleElement(Object arg0) { /* * (non-Javadoc) + * * @see com.fasterxml.jackson.databind.ser.ContainerSerializer#isEmpty(java.lang.Object) */ @Override @@ -274,7 +415,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 +425,204 @@ public JsonSerializer createContextual(SerializerProvider provider, BeanPrope 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; + } + } + + 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/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/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java b/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java new file mode 100644 index 000000000..84119efde --- /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(Object resource) { + return collectionResourceRel; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class) + */ + @Override + 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 00683f85d..7c2068918 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.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.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,19 @@ 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\"}}}]}}"; + + 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()); + Jackson1HalModule.HalHandlerInstantiator hi = new Jackson1HalModule.HalHandlerInstantiator(); + hi.setRelationResolver(new AnnotationRelProvider()); + mapper.setHandlerInstantiator(hi); } /** @@ -54,6 +66,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 +87,133 @@ 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)); + } + + @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 4e1e9adae..5826fc5ae 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -15,14 +15,20 @@ */ 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; +import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator; /** * Integration tests for Jackson 2 HAL integration. @@ -34,12 +40,19 @@ 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\"}}}]}}"; + + 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()); + HalHandlerInstantiator hi = new Jackson2HalModule.HalHandlerInstantiator(); + hi.setRelationResolver(new AnnotationRelProvider()); + mapper.setHandlerInstantiator(hi); } /** @@ -54,6 +67,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 +86,135 @@ 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)); + + } + + @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); + } + +} 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; + } + +}