Skip to content

Commit 48b039a

Browse files
committed
#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).
1 parent 7283721 commit 48b039a

File tree

6 files changed

+350
-2
lines changed

6 files changed

+350
-2
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858

5959
<properties>
6060
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
61-
<spring.version>3.1.2.RELEASE</spring.version>
61+
<spring.version>3.1.4.RELEASE</spring.version>
6262
<jackson1.version>1.9.10</jackson1.version>
6363
<jackson2.version>2.1.1</jackson2.version>
6464
<jaxrs.version>1.0</jaxrs.version>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.config;
17+
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Inherited;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.context.ApplicationContext;
26+
import org.springframework.context.annotation.Import;
27+
import org.springframework.hateoas.EntityLinks;
28+
import org.springframework.hateoas.LinkDiscoverer;
29+
30+
/**
31+
* Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for
32+
* injection to ease building hypermedia related code. Which components get registered depends on the hypermedia type
33+
* being activated through the {@link #type()} attribute. Hypermedia-type-specific implementations of the following
34+
* components will be registered:
35+
* <ul>
36+
* <li>{@link LinkDiscoverer}</li>
37+
* <li>a Jackson (1 or 2, dependning on what is on the classpath) module to correctly marshal the resource model classes
38+
* into the appropriate representation.
39+
* </ul>
40+
*
41+
* @see LinkDiscoverer
42+
* @see EntityLinks
43+
* @author Oliver Gierke
44+
*/
45+
@Retention(RetentionPolicy.RUNTIME)
46+
@Target(ElementType.TYPE)
47+
@Inherited
48+
@Documented
49+
@Import(HypermediaSupportBeanDefinitionRegistrar.class)
50+
public @interface EnableHypermediaSupport {
51+
52+
/**
53+
* The hypermedia type to be supported.
54+
*
55+
* @return
56+
*/
57+
HypermediaType type() default HypermediaType.DEFAULT;
58+
59+
/**
60+
* Hypermedia representation types supported.
61+
*
62+
* @author Oliver Gierke
63+
*/
64+
static enum HypermediaType {
65+
66+
DEFAULT,
67+
68+
/**
69+
* HAL - Hypermedia Application Language.
70+
*
71+
* @see http://stateless.co/hal_specification.html
72+
* @see http://tools.ietf.org/html/draft-kelly-json-hal-05
73+
*/
74+
HAL;
75+
}
76+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.config;
17+
18+
import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*;
19+
20+
import java.util.Map;
21+
22+
import org.springframework.beans.BeansException;
23+
import org.springframework.beans.factory.config.BeanDefinition;
24+
import org.springframework.beans.factory.config.BeanDefinitionHolder;
25+
import org.springframework.beans.factory.config.BeanPostProcessor;
26+
import org.springframework.beans.factory.support.AbstractBeanDefinition;
27+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
28+
import org.springframework.beans.factory.support.RootBeanDefinition;
29+
import org.springframework.context.ApplicationContext;
30+
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
31+
import org.springframework.core.type.AnnotationMetadata;
32+
import org.springframework.hateoas.EntityLinks;
33+
import org.springframework.hateoas.LinkDiscoverer;
34+
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
35+
import org.springframework.hateoas.core.DefaultLinkDiscoverer;
36+
import org.springframework.hateoas.hal.HalLinkDiscoverer;
37+
import org.springframework.hateoas.hal.Jackson1HalModule;
38+
import org.springframework.hateoas.hal.Jackson2HalModule;
39+
import org.springframework.util.ClassUtils;
40+
41+
import com.fasterxml.jackson.databind.ObjectMapper;
42+
43+
/**
44+
* {@link ImportBeanDefinitionRegistrar} implementation to activate hypermedia support based on the configured
45+
* hypermedia type. Activates {@link EntityLinks} support as well (essentially as if {@link EnableEntityLinks} was
46+
* activated as well).
47+
*
48+
* @author Oliver Gierke
49+
*/
50+
class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
51+
52+
private static final String LINK_DISCOVERER_BEAN_NAME = "_linkDiscoverer";
53+
54+
private static final boolean JACKSON1_PRESENT = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", null);
55+
private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
56+
null);
57+
58+
/*
59+
* (non-Javadoc)
60+
* @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry)
61+
*/
62+
@Override
63+
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
64+
65+
new LinkBuilderBeanDefinitionRegistrar().registerBeanDefinitions(importingClassMetadata, registry);
66+
67+
Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(EnableHypermediaSupport.class
68+
.getName());
69+
HypermediaType type = (HypermediaType) attributes.get("type");
70+
71+
registerBeanDefinition(new BeanDefinitionHolder(getLinkDiscovererBeanDefinition(type), LINK_DISCOVERER_BEAN_NAME),
72+
registry);
73+
74+
if (type == HypermediaType.HAL) {
75+
76+
if (JACKSON2_PRESENT) {
77+
registerWithGeneratedName(new RootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class), registry);
78+
}
79+
80+
if (JACKSON1_PRESENT) {
81+
registerWithGeneratedName(new RootBeanDefinition(Jackson1ModuleRegisteringBeanPostProcessor.class), registry);
82+
}
83+
}
84+
}
85+
86+
/**
87+
* Returns a {@link LinkDiscoverer} {@link BeanDefinition} suitable for the given {@link HypermediaType}.
88+
*
89+
* @param type
90+
* @return
91+
*/
92+
private AbstractBeanDefinition getLinkDiscovererBeanDefinition(HypermediaType type) {
93+
94+
AbstractBeanDefinition definition;
95+
96+
switch (type) {
97+
case HAL:
98+
definition = new RootBeanDefinition(HalLinkDiscoverer.class);
99+
break;
100+
case DEFAULT:
101+
default:
102+
definition = new RootBeanDefinition(DefaultLinkDiscoverer.class);
103+
}
104+
105+
definition.setSource(this);
106+
return definition;
107+
}
108+
109+
/**
110+
* {@link BeanPostProcessor} to register {@link Jackson2HalModule} with {@link ObjectMapper} instances registered in
111+
* the {@link ApplicationContext}.
112+
*
113+
* @author Oliver Gierke
114+
*/
115+
private static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor {
116+
117+
/*
118+
* (non-Javadoc)
119+
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String)
120+
*/
121+
@Override
122+
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
123+
return bean;
124+
}
125+
126+
/*
127+
* (non-Javadoc)
128+
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
129+
*/
130+
@Override
131+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
132+
133+
if (bean instanceof ObjectMapper) {
134+
((ObjectMapper) bean).registerModule(new Jackson2HalModule());
135+
}
136+
137+
return bean;
138+
}
139+
}
140+
141+
/**
142+
* {@link BeanPostProcessor} to register the {@link Jackson1HalModule} with
143+
* {@link org.codehaus.jackson.map.ObjectMapper} beans registered in the {@link ApplicationContext}.
144+
*
145+
* @author Oliver Gierke
146+
*/
147+
private static class Jackson1ModuleRegisteringBeanPostProcessor implements BeanPostProcessor {
148+
149+
/*
150+
* (non-Javadoc)
151+
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String)
152+
*/
153+
@Override
154+
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
155+
return bean;
156+
}
157+
158+
/*
159+
* (non-Javadoc)
160+
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
161+
*/
162+
@Override
163+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
164+
165+
if (bean instanceof org.codehaus.jackson.map.ObjectMapper) {
166+
((org.codehaus.jackson.map.ObjectMapper) bean).registerModule(new Jackson1HalModule());
167+
}
168+
169+
return bean;
170+
}
171+
}
172+
}

src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
*
4343
* @author Oliver Gierke
4444
*/
45-
public class LinkBuilderBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
45+
class LinkBuilderBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
4646

4747
private static final boolean IS_JAX_RS_PRESENT = ClassUtils.isPresent("javax.ws.rs.Path",
4848
ClassUtils.getDefaultClassLoader());
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.config;
17+
18+
import static org.hamcrest.Matchers.*;
19+
import static org.junit.Assert.*;
20+
import static org.mockito.Mockito.*;
21+
22+
import java.util.Map;
23+
24+
import org.hamcrest.Matchers;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
import org.mockito.Mockito;
28+
import org.mockito.runners.MockitoJUnitRunner;
29+
import org.springframework.context.ApplicationContext;
30+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
31+
import org.springframework.context.annotation.Bean;
32+
import org.springframework.context.annotation.Configuration;
33+
import org.springframework.hateoas.EntityLinks;
34+
import org.springframework.hateoas.LinkDiscoverer;
35+
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
36+
import org.springframework.hateoas.core.DefaultLinkDiscoverer;
37+
import org.springframework.hateoas.core.DelegatingEntityLinks;
38+
import org.springframework.hateoas.hal.HalLinkDiscoverer;
39+
import org.springframework.hateoas.hal.Jackson1HalModule;
40+
import org.springframework.hateoas.hal.Jackson2HalModule;
41+
42+
import com.fasterxml.jackson.databind.ObjectMapper;
43+
44+
/**
45+
* Integration tests for {@link EnableHypermediaSupport}.
46+
*
47+
* @author Oliver Gierke
48+
*/
49+
@RunWith(MockitoJUnitRunner.class)
50+
public class EnableHypermediaSupportIntegrationTest {
51+
52+
@Test
53+
public void bootstrapHalConfiguration() {
54+
55+
ApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class);
56+
assertEntityLinksSetUp(context);
57+
assertThat(context.getBean(LinkDiscoverer.class), is(instanceOf(HalLinkDiscoverer.class)));
58+
59+
ObjectMapper mapper = context.getBean(ObjectMapper.class);
60+
verify(mapper, times(1)).registerModule(Mockito.any(Jackson2HalModule.class));
61+
62+
org.codehaus.jackson.map.ObjectMapper jackson1Mapper = context.getBean(org.codehaus.jackson.map.ObjectMapper.class);
63+
verify(jackson1Mapper, times(1)).registerModule(Mockito.any(Jackson1HalModule.class));
64+
}
65+
66+
@Test
67+
public void bootstrapsDefaultConfiguration() {
68+
69+
ApplicationContext context = new AnnotationConfigApplicationContext(DefaultConfig.class);
70+
assertEntityLinksSetUp(context);
71+
assertThat(context.getBean(LinkDiscoverer.class), is(instanceOf(DefaultLinkDiscoverer.class)));
72+
}
73+
74+
private static void assertEntityLinksSetUp(ApplicationContext context) {
75+
76+
Map<String, EntityLinks> discoverers = context.getBeansOfType(EntityLinks.class);
77+
assertThat(discoverers.values(), hasItem(Matchers.<EntityLinks> instanceOf(DelegatingEntityLinks.class)));
78+
}
79+
80+
@Configuration
81+
@EnableHypermediaSupport(type = HypermediaType.HAL)
82+
static class HalConfig {
83+
84+
@Bean
85+
public ObjectMapper jackson2ObjectMapper() {
86+
return mock(ObjectMapper.class);
87+
}
88+
89+
@Bean
90+
public org.codehaus.jackson.map.ObjectMapper jackson1ObjectMapper() {
91+
return mock(org.codehaus.jackson.map.ObjectMapper.class);
92+
}
93+
}
94+
95+
@Configuration
96+
@EnableHypermediaSupport
97+
static class DefaultConfig {
98+
99+
}
100+
}

0 commit comments

Comments
 (0)