Permalink
Browse files

#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).
  • Loading branch information...
1 parent 35ad188 commit 3976ae168b82de6a10f8277ac1351e2be32e230a @olivergierke olivergierke committed Feb 15, 2013
View
@@ -58,7 +58,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <spring.version>3.1.2.RELEASE</spring.version>
+ <spring.version>3.1.4.RELEASE</spring.version>
<logback.version>1.0.9</logback.version>
<jackson1.version>1.9.10</jackson1.version>
<jackson2.version>2.1.1</jackson2.version>
@@ -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:
+ * <ul>
+ * <li>{@link LinkDiscoverer}</li>
+ * <li>a Jackson (1 or 2, dependning on what is on the classpath) module to correctly marshal the resource model classes
+ * into the appropriate representation.
+ * </ul>
+ *
+ * @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;
+ }
+}
@@ -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<String, Object> 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;
+ }
+ }
+}
@@ -42,7 +42,7 @@
*
* @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());
@@ -39,7 +39,7 @@
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
-public class ConfigIntegrationTest {
+public class EnableEntityLinksIntegrationTest {
@Configuration
@EnableEntityLinks
@@ -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<String, EntityLinks> discoverers = context.getBeansOfType(EntityLinks.class);
+ assertThat(discoverers.values(), hasItem(Matchers.<EntityLinks> 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 {
+
+ }
+}

0 comments on commit 3976ae1

Please sign in to comment.