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 { + + } +}