From 254339c3b24798bc3f42b4aa95af628e3ec14723 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 15 Apr 2013 17:31:08 +0200 Subject: [PATCH] #47, #60 - Implemented RelProvider registration into HAL serializers. Added @Relation annotation to be able to define the relations that shall be exposed. The annotation can be used on domain classes or ResourceSupport subtypes to define the relations that shall be used to build links pointing to them. Introduced RelProvider implementations that use the plain domain class name plus an appended List by default (DefaultRelProvider, fallback) as well as an AnnotationRelProvider that will inspect @Relation on the domain type. Added a DelegatingRelProvider that is based on a Spring Plugin PluginRegistry to allow adding RelProvider implementations transparently. Polished AnnotationRelProvider (added annotation caching) and corrected ControllerRelProvider to lookup relation types from Controller classes. Added BeanDefinition setup to HypermediaSupportBeanDefinitionRegistrar to setup a PluginRegistry of RelProviders and the wrapping DelegatingRelProvider to become the primary auto wiring candidate. Moved RelProvider registration into Jackson HandlerInstantiator implementations. Made the BeanPostProcessors for Jackson ObjectMapper customization aware of the BeanFactory to lookup the DelegatingRelProvider instance to hand them into the Jackson HandlerInstantiators. TODOs: - More tests - Correctly register ControllerRelProviders --- .../springframework/hateoas/RelProvider.java | 8 +- ...ermediaSupportBeanDefinitionRegistrar.java | 87 +++++++++++++++-- .../hateoas/core/AnnotationRelProvider.java | 91 +++++++++++++++++ .../hateoas/core/DefaultRelProvider.java | 58 +++++++++++ .../hateoas/core/DelegatingRelProvider.java | 61 ++++++++++++ .../hateoas/core/ObjectUtils.java | 46 +++++++++ .../hateoas/core/Relation.java | 52 ++++++++++ .../hateoas/hal/Jackson1HalModule.java | 79 +++++++++++++-- .../hateoas/hal/Jackson2HalModule.java | 97 +++++++++++++++++-- .../hateoas/mvc/ControllerRelProvider.java | 31 ++++-- ...nableHypermediaSupportIntegrationTest.java | 9 ++ .../core/DelegatingRelProviderUnitTests.java | 55 +++++++++++ .../hal/Jackson1HalIntegrationTest.java | 37 ++++++- .../hal/Jackson2HalIntegrationTest.java | 43 +++++++- .../hateoas/hal/SimpleAnnotatedPojo.java | 16 +++ 15 files changed, 731 insertions(+), 39 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/core/AnnotationRelProvider.java create mode 100644 src/main/java/org/springframework/hateoas/core/DefaultRelProvider.java create mode 100644 src/main/java/org/springframework/hateoas/core/DelegatingRelProvider.java create mode 100644 src/main/java/org/springframework/hateoas/core/ObjectUtils.java create mode 100644 src/main/java/org/springframework/hateoas/core/Relation.java create mode 100644 src/test/java/org/springframework/hateoas/core/DelegatingRelProviderUnitTests.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 a9bb3285..342b6962 100644 --- a/src/main/java/org/springframework/hateoas/RelProvider.java +++ b/src/main/java/org/springframework/hateoas/RelProvider.java @@ -15,12 +15,14 @@ */ package org.springframework.hateoas; +import org.springframework.plugin.core.Plugin; + /** * @author Oliver Gierke */ -public interface RelProvider { +public interface RelProvider extends Plugin> { - String getRelForCollectionResource(Class type); + String getSingleResourceRelFor(Class type); - String getRelForSingleResource(Class type); + String getCollectionResourceRelFor(Class type); } diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index f4bd302d..817e12f2 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -21,10 +21,13 @@ import java.util.List; import java.util.Map; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; @@ -32,14 +35,20 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.core.AnnotationRelProvider; import org.springframework.hateoas.core.DefaultLinkDiscoverer; +import org.springframework.hateoas.core.DefaultRelProvider; +import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.hal.HalLinkDiscoverer; import org.springframework.hateoas.hal.Jackson1HalModule; import org.springframework.hateoas.hal.Jackson2HalModule; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.plugin.core.support.PluginRegistryFactoryBean; import org.springframework.util.ClassUtils; import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; @@ -56,12 +65,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { private static final String LINK_DISCOVERER_BEAN_NAME = "_linkDiscoverer"; + private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider"; private static final boolean JACKSON1_PRESENT = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", null); private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", null); private static final boolean JSONPATH_PRESENT = ClassUtils.isPresent("com.jayway.jsonpath.JsonPath", null); + private final ImportBeanDefinitionRegistrar linkBuilderBeanDefinitionRegistrar = new LinkBuilderBeanDefinitionRegistrar(); + /* * (non-Javadoc) * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) @@ -69,7 +81,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - new LinkBuilderBeanDefinitionRegistrar().registerBeanDefinitions(importingClassMetadata, registry); + linkBuilderBeanDefinitionRegistrar.registerBeanDefinitions(importingClassMetadata, registry); Map attributes = importingClassMetadata.getAnnotationAttributes(EnableHypermediaSupport.class .getName()); @@ -90,6 +102,38 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe registerWithGeneratedName(new RootBeanDefinition(Jackson1ModuleRegisteringBeanPostProcessor.class), registry); } } + + registerRelProviderPluginRegistryAndDelegate(registry); + } + + /** + * Registers bean definitions for a {@link PluginRegistry} to capture {@link RelProvider} instances. Wraps the + * registry into a {@link DelegatingRelProvider} bean definition backed by the registry. + * + * @param registry + */ + private static void registerRelProviderPluginRegistryAndDelegate(BeanDefinitionRegistry registry) { + + RootBeanDefinition defaultRelProviderBeanDefinition = new RootBeanDefinition(DefaultRelProvider.class); + registry.registerBeanDefinition("defaultRelProvider", defaultRelProviderBeanDefinition); + + RootBeanDefinition annotationRelProviderBeanDefinition = new RootBeanDefinition(AnnotationRelProvider.class); + registry.registerBeanDefinition("annotationRelProvider", annotationRelProviderBeanDefinition); + + BeanDefinitionBuilder registryFactoryBeanBuilder = BeanDefinitionBuilder + .rootBeanDefinition(PluginRegistryFactoryBean.class); + registryFactoryBeanBuilder.addPropertyValue("type", RelProvider.class); + registryFactoryBeanBuilder.addPropertyValue("exclusions", DelegatingRelProvider.class); + + AbstractBeanDefinition registryBeanDefinition = registryFactoryBeanBuilder.getBeanDefinition(); + registry.registerBeanDefinition("relProviderPluginRegistry", registryBeanDefinition); + + BeanDefinitionBuilder delegateBuilder = BeanDefinitionBuilder.rootBeanDefinition(DelegatingRelProvider.class); + delegateBuilder.addConstructorArgValue(registryBeanDefinition); + + AbstractBeanDefinition beanDefinition = delegateBuilder.getBeanDefinition(); + beanDefinition.setPrimary(true); + registry.registerBeanDefinition(DELEGATING_REL_PROVIDER_BEAN_NAME, beanDefinition); } /** @@ -121,7 +165,17 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe * * @author Oliver Gierke */ - private static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor { + private static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { + + private BeanFactory factory; + + /* (non-Javadoc) + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.factory = beanFactory; + } /* * (non-Javadoc) @@ -164,7 +218,12 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe } private void registerModule(Object objectMapper) { - ((ObjectMapper) objectMapper).registerModule(new Jackson2HalModule(null)); + + RelProvider provider = factory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class); + + ObjectMapper mapper = (ObjectMapper) objectMapper; + mapper.registerModule(new Jackson2HalModule()); + mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(provider)); } } @@ -174,7 +233,18 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe * * @author Oliver Gierke */ - private static class Jackson1ModuleRegisteringBeanPostProcessor implements BeanPostProcessor { + private static class Jackson1ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware { + + private BeanFactory beanFactory; + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } /* * (non-Javadoc) @@ -216,8 +286,13 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe } } - private void registerModule(Object mapper) { - ((org.codehaus.jackson.map.ObjectMapper) mapper).registerModule(new Jackson1HalModule(null)); + private void registerModule(Object objectMapper) { + + RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class); + + org.codehaus.jackson.map.ObjectMapper mapper = (org.codehaus.jackson.map.ObjectMapper) objectMapper; + mapper.registerModule(new Jackson1HalModule()); + mapper.setHandlerInstantiator(new Jackson1HalModule.HalHandlerInstantiator(relProvider)); } } } diff --git a/src/main/java/org/springframework/hateoas/core/AnnotationRelProvider.java b/src/main/java/org/springframework/hateoas/core/AnnotationRelProvider.java new file mode 100644 index 00000000..5df3cdb7 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/AnnotationRelProvider.java @@ -0,0 +1,91 @@ +/* + * 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.core; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.hateoas.RelProvider; + +/** + * @author Oliver Gierke + * @author Alexander Baetz + */ +@Order(100) +public class AnnotationRelProvider implements RelProvider { + + private final Map, Relation> annotationCache = new HashMap, Relation>(); + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class) + */ + @Override + public String getCollectionResourceRelFor(Class type) { + + Relation annotation = lookupAnnotation(type); + + if (annotation == null || Relation.NO_RELATION.equals(annotation.collectionRelation())) { + return null; + } + + return annotation.collectionRelation(); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Object) + */ + @Override + public String getSingleResourceRelFor(Class type) { + + Relation annotation = lookupAnnotation(type); + + if (annotation == null || Relation.NO_RELATION.equals(annotation.value())) { + return null; + } + + return annotation.value(); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return lookupAnnotation(delimiter) != null; + } + + private Relation lookupAnnotation(Class type) { + + Relation relation = annotationCache.get(type); + + if (relation != null) { + return relation; + } + + relation = AnnotationUtils.getAnnotation(type, Relation.class); + + if (relation != null) { + annotationCache.put(type, relation); + } + + return relation; + } +} diff --git a/src/main/java/org/springframework/hateoas/core/DefaultRelProvider.java b/src/main/java/org/springframework/hateoas/core/DefaultRelProvider.java new file mode 100644 index 00000000..14fa691a --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/DefaultRelProvider.java @@ -0,0 +1,58 @@ +/* + * 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.core; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.hateoas.RelProvider; +import org.springframework.util.StringUtils; + +/** + * Default implementation of {@link RelProvider} to simply use the uncapitalized version of the given type's name as + * single resource rel as well as an appended {@code List} for the collection resource rel. + * + * @author Oliver Gierke + */ +@Order(Ordered.LOWEST_PRECEDENCE) +public class DefaultRelProvider implements RelProvider { + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class) + */ + @Override + public String getCollectionResourceRelFor(Class type) { + return StringUtils.uncapitalize(type.getSimpleName()) + "List"; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class) + */ + @Override + public String getSingleResourceRelFor(Class type) { + return StringUtils.uncapitalize(type.getSimpleName()); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/DelegatingRelProvider.java b/src/main/java/org/springframework/hateoas/core/DelegatingRelProvider.java new file mode 100644 index 00000000..a27dca5d --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/DelegatingRelProvider.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.core; + +import org.springframework.hateoas.RelProvider; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.util.Assert; + +/** + * @author Oliver Gierke + */ +public class DelegatingRelProvider implements RelProvider { + + private final PluginRegistry> providers; + + public DelegatingRelProvider(PluginRegistry> providers) { + + Assert.notNull(providers, "RelProviders must not be null!"); + this.providers = providers; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class) + */ + @Override + public String getSingleResourceRelFor(Class type) { + return providers.getPluginFor(type).getSingleResourceRelFor(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class) + */ + @Override + public String getCollectionResourceRelFor(java.lang.Class type) { + return providers.getPluginFor(type).getCollectionResourceRelFor(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(java.lang.Class delimiter) { + return providers.hasPluginFor(delimiter); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/ObjectUtils.java b/src/main/java/org/springframework/hateoas/core/ObjectUtils.java new file mode 100644 index 00000000..0ed3e5d5 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/ObjectUtils.java @@ -0,0 +1,46 @@ +/* + * 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.core; + +import org.springframework.hateoas.Resource; + +/** + * Simple helper class. + * + * @author Oliver Gierke + */ +public class ObjectUtils { + + /** + * Returns the resource type of the given object. In case a {@link Resource} is given, it will return the + * {@link Resource} content's type. + * + * @param object can be {@literal null}. + * @return + */ + public static Class getResourceType(Object object) { + + if (object instanceof Resource) { + return nullSafeType(((Resource) object).getContent()); + } + + return nullSafeType(object); + } + + private static Class nullSafeType(Object object) { + return object == null ? null : object.getClass(); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/Relation.java b/src/main/java/org/springframework/hateoas/core/Relation.java new file mode 100644 index 00000000..b8782ef0 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/Relation.java @@ -0,0 +1,52 @@ +/* + * 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.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; + +/** + * Annotation to configure the relation to be used when embedding objects in HAL representations of {@link Resource}s + * and {@link Resources}. + * + * @author Alexander Baetz + * @author Oliver Gierke + */ +@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Relation { + + static final String NO_RELATION = ""; + + /** + * Defines the relation to be used when referring to a single resource. + * + * @return + */ + String value() default NO_RELATION; + + /** + * Defines the relation to be used when referring to a collection of resources. + * + * @return + */ + String collectionRelation() default NO_RELATION; +} diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java index 74edd347..202e1e58 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson1HalModule.java @@ -35,24 +35,32 @@ 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.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.beans.BeanUtils; 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.hateoas.core.ObjectUtils; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -66,18 +74,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 +201,12 @@ public class Jackson1HalModule extends SimpleModule { for (Object resource : value) { - String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass()); + Class type = ObjectUtils.getResourceType(resource); + String relation = relProvider == null ? "content" : relProvider.getSingleResourceRelFor(type); + + if (relation == null) { + relation = "content"; + } if (sortedLinks.get(relation) == null) { sortedLinks.put(relation, new ArrayList()); @@ -454,4 +462,59 @@ public class Jackson1HalModule extends SimpleModule { return des; } } + + public static class HalHandlerInstantiator extends HandlerInstantiator { + + private final Map, Object> instanceMap = new HashMap, Object>(); + + public HalHandlerInstantiator(RelProvider relProvider) { + + Assert.notNull(relProvider, "RelProvider must not be null!"); + this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(null, relProvider)); + + } + + @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); + } + + private Object findInstance(Class type, boolean createInstance) { + + Object result = instanceMap.get(type); + + if (result != null) { + return result; + } + + return createInstance ? BeanUtils.instantiateClass(type) : null; + } + } } diff --git a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java index c1491ad0..edc3db2e 100644 --- a/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java +++ b/src/main/java/org/springframework/hateoas/hal/Jackson2HalModule.java @@ -23,11 +23,14 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import org.springframework.beans.BeanUtils; 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.hateoas.core.ObjectUtils; +import org.springframework.util.Assert; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; @@ -37,17 +40,24 @@ 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.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 +73,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); } /** @@ -198,6 +204,8 @@ public class Jackson2HalModule extends SimpleModule { */ public static class HalResourcesSerializer extends ContainerSerializer> implements ContextualSerializer { + private static final String DEFAULT_REL = "content"; + private final BeanProperty property; private final RelProvider relProvider; @@ -234,12 +242,16 @@ public class Jackson2HalModule extends SimpleModule { for (Object resource : value) { - // TODO: do something fancy to get the relation name - String relation = relProvider == null ? "content" : relProvider.getRelForSingleResource(value.getClass()); + Class type = ObjectUtils.getResourceType(resource); + String relation = relProvider == null ? DEFAULT_REL : relProvider.getSingleResourceRelFor(type); + + if (relation == null) { + relation = DEFAULT_REL; + } + if (sortedLinks.get(relation) == null) { sortedLinks.put(relation, new ArrayList()); } - sortedLinks.get(relation).add(resource); } @@ -257,7 +269,7 @@ public class Jackson2HalModule extends SimpleModule { @Override public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - return new HalResourcesSerializer(property, null); + return new HalResourcesSerializer(property, relProvider); } @Override @@ -564,4 +576,69 @@ public class Jackson2HalModule extends SimpleModule { return des; } } + + public static class HalHandlerInstantiator extends HandlerInstantiator { + + private final Map, Object> instanceMap = new HashMap, Object>(); + + public HalHandlerInstantiator(RelProvider resolver) { + + Assert.notNull(resolver, "RelProvider must not be null!"); + this.instanceMap.put(HalResourcesSerializer.class, new HalResourcesSerializer(null, resolver)); + } + + private Object findInstance(Class type) { + + Object result = instanceMap.get(type); + return result != null ? result : BeanUtils.instantiateClass(type); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#deserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public JsonDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, + Class deserClass) { + return (JsonDeserializer) findInstance(deserClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#keyDeserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, + Class keyDeserClass) { + return (KeyDeserializer) findInstance(keyDeserClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#serializerInstance(com.fasterxml.jackson.databind.SerializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public JsonSerializer serializerInstance(SerializationConfig config, Annotated annotated, Class serClass) { + return (JsonSerializer) findInstance(serClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeResolverBuilderInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @Override + public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, + Class builderClass) { + return (TypeResolverBuilder) findInstance(builderClass); + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeIdResolverInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class) + */ + @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 1d18b58a..9e29a16f 100644 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerRelProvider.java @@ -18,26 +18,28 @@ package org.springframework.hateoas.mvc; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.hateoas.ExposesResourceFor; import org.springframework.hateoas.RelProvider; +import org.springframework.plugin.core.PluginRegistry; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * @author Oliver Gierke */ public class ControllerRelProvider implements RelProvider { + private final Class controllerType; private final Class entityType; - private final String collectionResourceRel; - private final String singleResourceRel; + private final PluginRegistry> providers; - public ControllerRelProvider(Class controller) { + public ControllerRelProvider(Class controller, PluginRegistry> providers) { + + Assert.notNull(controller); ExposesResourceFor annotation = AnnotationUtils.findAnnotation(controller, ExposesResourceFor.class); Assert.notNull(annotation); + this.controllerType = controller; this.entityType = annotation.value(); - this.singleResourceRel = StringUtils.uncapitalize(entityType.getSimpleName()); - this.collectionResourceRel = singleResourceRel + "List"; + this.providers = providers; } /* @@ -45,8 +47,8 @@ public class ControllerRelProvider implements RelProvider { * @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class) */ @Override - public String getRelForCollectionResource(Class type) { - return collectionResourceRel; + public String getCollectionResourceRelFor(Class resource) { + return providers.getPluginFor(entityType).getCollectionResourceRelFor(resource); } /* @@ -54,7 +56,16 @@ public class ControllerRelProvider implements RelProvider { * @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class) */ @Override - public String getRelForSingleResource(Class type) { - return singleResourceRel; + public String getSingleResourceRelFor(Class resource) { + return providers.getPluginFor(entityType).getSingleResourceRelFor(resource); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return controllerType.equals(delimiter); } } diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index 47d5a3a1..27400cae 100644 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -32,9 +32,11 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.core.DefaultLinkDiscoverer; import org.springframework.hateoas.core.DelegatingEntityLinks; +import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.hal.HalLinkDiscoverer; import org.springframework.hateoas.hal.Jackson1HalModule; import org.springframework.hateoas.hal.Jackson2HalModule; @@ -68,6 +70,7 @@ public class EnableHypermediaSupportIntegrationTest { ApplicationContext context = new AnnotationConfigApplicationContext(DefaultConfig.class); assertEntityLinksSetUp(context); + assertRelProvidersSetUp(context); assertThat(context.getBean(LinkDiscoverer.class), is(instanceOf(DefaultLinkDiscoverer.class))); } @@ -77,6 +80,12 @@ public class EnableHypermediaSupportIntegrationTest { assertThat(discoverers.values(), hasItem(Matchers. instanceOf(DelegatingEntityLinks.class))); } + private static void assertRelProvidersSetUp(ApplicationContext context) { + + Map discoverers = context.getBeansOfType(RelProvider.class); + assertThat(discoverers.values(), hasItem(Matchers. instanceOf(DelegatingRelProvider.class))); + } + @Configuration @EnableHypermediaSupport(type = HypermediaType.HAL) static class HalConfig { diff --git a/src/test/java/org/springframework/hateoas/core/DelegatingRelProviderUnitTests.java b/src/test/java/org/springframework/hateoas/core/DelegatingRelProviderUnitTests.java new file mode 100644 index 00000000..8f2e62e3 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/core/DelegatingRelProviderUnitTests.java @@ -0,0 +1,55 @@ +/* + * 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.core; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.springframework.hateoas.RelProvider; +import org.springframework.plugin.core.OrderAwarePluginRegistry; +import org.springframework.plugin.core.PluginRegistry; + +/** + * @author Oliver Gierke + */ +public class DelegatingRelProviderUnitTests { + + @Test + public void foo() { + + List providers = Arrays.asList(new AnnotationRelProvider(), new DefaultRelProvider()); + PluginRegistry> registry = OrderAwarePluginRegistry.create(providers); + + RelProvider delegatingProvider = new DelegatingRelProvider(registry); + + assertThat(delegatingProvider.supports(Sample.class), is(true)); + assertThat(delegatingProvider.getSingleResourceRelFor(Sample.class), is("foo")); + assertThat(delegatingProvider.getCollectionResourceRelFor(Sample.class), is("bar")); + + assertThat(delegatingProvider.supports(String.class), is(true)); + assertThat(delegatingProvider.getSingleResourceRelFor(String.class), is("string")); + assertThat(delegatingProvider.getCollectionResourceRelFor(String.class), is("stringList")); + } + + @Relation(value = "foo", collectionRelation = "bar") + static class Sample { + + } +} diff --git a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java index 0832ccb7..84b97306 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson1HalIntegrationTest.java @@ -28,6 +28,8 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.hateoas.core.AnnotationRelProvider; +import org.springframework.hateoas.hal.Jackson1HalModule.HalHandlerInstantiator; /** * Integration tests for Jackson 1 based HAL integration. @@ -44,9 +46,13 @@ 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()); + mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider())); } /** @@ -181,4 +187,33 @@ public class Jackson1HalIntegrationTest extends AbstractMarshallingIntegrationTe 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 bb27efff..595303dc 100644 --- a/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/Jackson2HalIntegrationTest.java @@ -28,6 +28,8 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.hateoas.core.AnnotationRelProvider; +import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator; /** * Integration tests for Jackson 2 HAL integration. @@ -44,9 +46,13 @@ 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()); + mapper.setHandlerInstantiator(new HalHandlerInstantiator(new AnnotationRelProvider())); } /** @@ -182,4 +188,39 @@ public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingInteg assertThat(result, is(expected)); } + + /** + * @see #47, #60 + */ + @Test + public void serializesAnnotatedResourceResourcesAsEmbedded() 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)); + } + + /** + * @see #47, #60 + */ + @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 00000000..f611a344 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/hal/SimpleAnnotatedPojo.java @@ -0,0 +1,16 @@ +package org.springframework.hateoas.hal; + +import org.springframework.hateoas.core.Relation; + +@Relation(value = "pojo", collectionRelation = "pojo") +public class SimpleAnnotatedPojo extends SimplePojo { + + public SimpleAnnotatedPojo() { + } + + public SimpleAnnotatedPojo(String text, int number) { + setText(text); + setNumber(number); + } + +}