From 3976ae168b82de6a10f8277ac1351e2be32e230a Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 15 Feb 2013 11:21:23 +0100 Subject: [PATCH] #50 - Added @EnableHypermediaSupport annotation. The annotation registers supporting application components as Spring beans based on the configured hypermedia type. Currently supported are the default rendering as well as HAL. We register a matching LinkDiscoverer implementation as well as the appropriate Jackson modules (if present on the classpath) to support HAL. Upgraded to Spring 3.1.4 to benefit from fix to prevent multiple invocations of ImportBeanDefinitionRegistrars (see SPR-9939 / SPR-9925). --- pom.xml | 2 +- .../config/EnableHypermediaSupport.java | 76 ++++++++ ...ermediaSupportBeanDefinitionRegistrar.java | 172 ++++++++++++++++++ .../LinkBuilderBeanDefinitionRegistrar.java | 2 +- ... => EnableEntityLinksIntegrationTest.java} | 2 +- ...nableHypermediaSupportIntegrationTest.java | 100 ++++++++++ 6 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java create mode 100644 src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java rename src/test/java/org/springframework/hateoas/config/{ConfigIntegrationTest.java => EnableEntityLinksIntegrationTest.java} (97%) create mode 100644 src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java diff --git a/pom.xml b/pom.xml index 2b4e124d..e802cbcf 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ UTF-8 - 3.1.2.RELEASE + 3.1.4.RELEASE 1.0.9 1.9.10 2.1.1 diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java new file mode 100644 index 00000000..4f197cfa --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java @@ -0,0 +1,76 @@ +/* + * 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.config; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.LinkDiscoverer; + +/** + * Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for + * injection to ease building hypermedia related code. Which components get registered depends on the hypermedia type + * being activated through the {@link #type()} attribute. Hypermedia-type-specific implementations of the following + * components will be registered: + * + * + * @see LinkDiscoverer + * @see EntityLinks + * @author Oliver Gierke + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +@Documented +@Import(HypermediaSupportBeanDefinitionRegistrar.class) +public @interface EnableHypermediaSupport { + + /** + * The hypermedia type to be supported. + * + * @return + */ + HypermediaType type() default HypermediaType.DEFAULT; + + /** + * Hypermedia representation types supported. + * + * @author Oliver Gierke + */ + static enum HypermediaType { + + DEFAULT, + + /** + * HAL - Hypermedia Application Language. + * + * @see http://stateless.co/hal_specification.html + * @see http://tools.ietf.org/html/draft-kelly-json-hal-05 + */ + HAL; + } +} diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java new file mode 100644 index 00000000..f2d6e583 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -0,0 +1,172 @@ +/* + * 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.config; + +import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*; + +import java.util.Map; + +import org.springframework.beans.BeansException; +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.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +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.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.core.DefaultLinkDiscoverer; +import org.springframework.hateoas.hal.HalLinkDiscoverer; +import org.springframework.hateoas.hal.Jackson1HalModule; +import org.springframework.hateoas.hal.Jackson2HalModule; +import org.springframework.util.ClassUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link ImportBeanDefinitionRegistrar} implementation to activate hypermedia support based on the configured + * hypermedia type. Activates {@link EntityLinks} support as well (essentially as if {@link EnableEntityLinks} was + * activated as well). + * + * @author Oliver Gierke + */ +class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { + + private static final String LINK_DISCOVERER_BEAN_NAME = "_linkDiscoverer"; + + 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); + + /* + * (non-Javadoc) + * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + + new LinkBuilderBeanDefinitionRegistrar().registerBeanDefinitions(importingClassMetadata, registry); + + Map attributes = importingClassMetadata.getAnnotationAttributes(EnableHypermediaSupport.class + .getName()); + HypermediaType type = (HypermediaType) attributes.get("type"); + + registerBeanDefinition(new BeanDefinitionHolder(getLinkDiscovererBeanDefinition(type), LINK_DISCOVERER_BEAN_NAME), + registry); + + if (type == HypermediaType.HAL) { + + if (JACKSON2_PRESENT) { + registerWithGeneratedName(new RootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class), registry); + } + + if (JACKSON1_PRESENT) { + registerWithGeneratedName(new RootBeanDefinition(Jackson1ModuleRegisteringBeanPostProcessor.class), registry); + } + } + } + + /** + * Returns a {@link LinkDiscoverer} {@link BeanDefinition} suitable for the given {@link HypermediaType}. + * + * @param type + * @return + */ + private AbstractBeanDefinition getLinkDiscovererBeanDefinition(HypermediaType type) { + + AbstractBeanDefinition definition; + + switch (type) { + case HAL: + definition = new RootBeanDefinition(HalLinkDiscoverer.class); + break; + case DEFAULT: + default: + definition = new RootBeanDefinition(DefaultLinkDiscoverer.class); + } + + definition.setSource(this); + return definition; + } + + /** + * {@link BeanPostProcessor} to register {@link Jackson2HalModule} with {@link ObjectMapper} instances registered in + * the {@link ApplicationContext}. + * + * @author Oliver Gierke + */ + private static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor { + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String) + */ + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String) + */ + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + if (bean instanceof ObjectMapper) { + ((ObjectMapper) bean).registerModule(new Jackson2HalModule()); + } + + return bean; + } + } + + /** + * {@link BeanPostProcessor} to register the {@link Jackson1HalModule} with + * {@link org.codehaus.jackson.map.ObjectMapper} beans registered in the {@link ApplicationContext}. + * + * @author Oliver Gierke + */ + private static class Jackson1ModuleRegisteringBeanPostProcessor implements BeanPostProcessor { + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String) + */ + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String) + */ + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + + if (bean instanceof org.codehaus.jackson.map.ObjectMapper) { + ((org.codehaus.jackson.map.ObjectMapper) bean).registerModule(new Jackson1HalModule()); + } + + return bean; + } + } +} diff --git a/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java index edd89a7f..48fc4e71 100644 --- a/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java @@ -42,7 +42,7 @@ import org.springframework.util.ClassUtils; * * @author Oliver Gierke */ -public class LinkBuilderBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { +class LinkBuilderBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { private static final boolean IS_JAX_RS_PRESENT = ClassUtils.isPresent("javax.ws.rs.Path", ClassUtils.getDefaultClassLoader()); diff --git a/src/test/java/org/springframework/hateoas/config/ConfigIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableEntityLinksIntegrationTest.java similarity index 97% rename from src/test/java/org/springframework/hateoas/config/ConfigIntegrationTest.java rename to src/test/java/org/springframework/hateoas/config/EnableEntityLinksIntegrationTest.java index 4468497a..3443bc95 100644 --- a/src/test/java/org/springframework/hateoas/config/ConfigIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableEntityLinksIntegrationTest.java @@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping; */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration -public class ConfigIntegrationTest { +public class EnableEntityLinksIntegrationTest { @Configuration @EnableEntityLinks diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java new file mode 100644 index 00000000..47d5a3a1 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -0,0 +1,100 @@ +/* + * 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.config; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.Map; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +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.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.core.DefaultLinkDiscoverer; +import org.springframework.hateoas.core.DelegatingEntityLinks; +import org.springframework.hateoas.hal.HalLinkDiscoverer; +import org.springframework.hateoas.hal.Jackson1HalModule; +import org.springframework.hateoas.hal.Jackson2HalModule; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Integration tests for {@link EnableHypermediaSupport}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class EnableHypermediaSupportIntegrationTest { + + @Test + public void bootstrapHalConfiguration() { + + ApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); + assertEntityLinksSetUp(context); + assertThat(context.getBean(LinkDiscoverer.class), is(instanceOf(HalLinkDiscoverer.class))); + + ObjectMapper mapper = context.getBean(ObjectMapper.class); + verify(mapper, times(1)).registerModule(Mockito.any(Jackson2HalModule.class)); + + org.codehaus.jackson.map.ObjectMapper jackson1Mapper = context.getBean(org.codehaus.jackson.map.ObjectMapper.class); + verify(jackson1Mapper, times(1)).registerModule(Mockito.any(Jackson1HalModule.class)); + } + + @Test + public void bootstrapsDefaultConfiguration() { + + ApplicationContext context = new AnnotationConfigApplicationContext(DefaultConfig.class); + assertEntityLinksSetUp(context); + assertThat(context.getBean(LinkDiscoverer.class), is(instanceOf(DefaultLinkDiscoverer.class))); + } + + private static void assertEntityLinksSetUp(ApplicationContext context) { + + Map discoverers = context.getBeansOfType(EntityLinks.class); + assertThat(discoverers.values(), hasItem(Matchers. instanceOf(DelegatingEntityLinks.class))); + } + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class HalConfig { + + @Bean + public ObjectMapper jackson2ObjectMapper() { + return mock(ObjectMapper.class); + } + + @Bean + public org.codehaus.jackson.map.ObjectMapper jackson1ObjectMapper() { + return mock(org.codehaus.jackson.map.ObjectMapper.class); + } + } + + @Configuration + @EnableHypermediaSupport + static class DefaultConfig { + + } +}