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); + } + +}