Skip to content

Commit 254339c

Browse files
committed
#47, #60 - Implemented RelProvider registration into HAL serializers.
Added @relation annotation to be able to define the relations that shall be exposed. The annotation can be used on domain classes or ResourceSupport subtypes to define the relations that shall be used to build links pointing to them. Introduced RelProvider implementations that use the plain domain class name plus an appended List by default (DefaultRelProvider, fallback) as well as an AnnotationRelProvider that will inspect @relation on the domain type. Added a DelegatingRelProvider that is based on a Spring Plugin PluginRegistry to allow adding RelProvider implementations transparently. Polished AnnotationRelProvider (added annotation caching) and corrected ControllerRelProvider to lookup relation types from Controller classes. Added BeanDefinition setup to HypermediaSupportBeanDefinitionRegistrar to setup a PluginRegistry of RelProviders and the wrapping DelegatingRelProvider to become the primary auto wiring candidate. Moved RelProvider registration into Jackson HandlerInstantiator implementations. Made the BeanPostProcessors for Jackson ObjectMapper customization aware of the BeanFactory to lookup the DelegatingRelProvider instance to hand them into the Jackson HandlerInstantiators. TODOs: - More tests - Correctly register ControllerRelProviders
1 parent 47b5026 commit 254339c

15 files changed

+731
-39
lines changed

src/main/java/org/springframework/hateoas/RelProvider.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
*/
1616
package org.springframework.hateoas;
1717

18+
import org.springframework.plugin.core.Plugin;
19+
1820
/**
1921
* @author Oliver Gierke
2022
*/
21-
public interface RelProvider {
23+
public interface RelProvider extends Plugin<Class<?>> {
2224

23-
String getRelForCollectionResource(Class<?> type);
25+
String getSingleResourceRelFor(Class<?> type);
2426

25-
String getRelForSingleResource(Class<?> type);
27+
String getCollectionResourceRelFor(Class<?> type);
2628
}

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

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,34 @@
2121
import java.util.Map;
2222

2323
import org.springframework.beans.BeansException;
24+
import org.springframework.beans.factory.BeanFactory;
25+
import org.springframework.beans.factory.BeanFactoryAware;
2426
import org.springframework.beans.factory.config.BeanDefinition;
2527
import org.springframework.beans.factory.config.BeanDefinitionHolder;
2628
import org.springframework.beans.factory.config.BeanPostProcessor;
2729
import org.springframework.beans.factory.support.AbstractBeanDefinition;
30+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
2831
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2932
import org.springframework.beans.factory.support.RootBeanDefinition;
3033
import org.springframework.context.ApplicationContext;
3134
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
3235
import org.springframework.core.type.AnnotationMetadata;
3336
import org.springframework.hateoas.EntityLinks;
3437
import org.springframework.hateoas.LinkDiscoverer;
38+
import org.springframework.hateoas.RelProvider;
3539
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
40+
import org.springframework.hateoas.core.AnnotationRelProvider;
3641
import org.springframework.hateoas.core.DefaultLinkDiscoverer;
42+
import org.springframework.hateoas.core.DefaultRelProvider;
43+
import org.springframework.hateoas.core.DelegatingRelProvider;
3744
import org.springframework.hateoas.hal.HalLinkDiscoverer;
3845
import org.springframework.hateoas.hal.Jackson1HalModule;
3946
import org.springframework.hateoas.hal.Jackson2HalModule;
4047
import org.springframework.http.converter.HttpMessageConverter;
4148
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
4249
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
50+
import org.springframework.plugin.core.PluginRegistry;
51+
import org.springframework.plugin.core.support.PluginRegistryFactoryBean;
4352
import org.springframework.util.ClassUtils;
4453
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;
4554
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
@@ -56,20 +65,23 @@
5665
class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
5766

5867
private static final String LINK_DISCOVERER_BEAN_NAME = "_linkDiscoverer";
68+
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
5969

6070
private static final boolean JACKSON1_PRESENT = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", null);
6171
private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
6272
null);
6373
private static final boolean JSONPATH_PRESENT = ClassUtils.isPresent("com.jayway.jsonpath.JsonPath", null);
6474

75+
private final ImportBeanDefinitionRegistrar linkBuilderBeanDefinitionRegistrar = new LinkBuilderBeanDefinitionRegistrar();
76+
6577
/*
6678
* (non-Javadoc)
6779
* @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry)
6880
*/
6981
@Override
7082
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
7183

72-
new LinkBuilderBeanDefinitionRegistrar().registerBeanDefinitions(importingClassMetadata, registry);
84+
linkBuilderBeanDefinitionRegistrar.registerBeanDefinitions(importingClassMetadata, registry);
7385

7486
Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(EnableHypermediaSupport.class
7587
.getName());
@@ -90,6 +102,38 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B
90102
registerWithGeneratedName(new RootBeanDefinition(Jackson1ModuleRegisteringBeanPostProcessor.class), registry);
91103
}
92104
}
105+
106+
registerRelProviderPluginRegistryAndDelegate(registry);
107+
}
108+
109+
/**
110+
* Registers bean definitions for a {@link PluginRegistry} to capture {@link RelProvider} instances. Wraps the
111+
* registry into a {@link DelegatingRelProvider} bean definition backed by the registry.
112+
*
113+
* @param registry
114+
*/
115+
private static void registerRelProviderPluginRegistryAndDelegate(BeanDefinitionRegistry registry) {
116+
117+
RootBeanDefinition defaultRelProviderBeanDefinition = new RootBeanDefinition(DefaultRelProvider.class);
118+
registry.registerBeanDefinition("defaultRelProvider", defaultRelProviderBeanDefinition);
119+
120+
RootBeanDefinition annotationRelProviderBeanDefinition = new RootBeanDefinition(AnnotationRelProvider.class);
121+
registry.registerBeanDefinition("annotationRelProvider", annotationRelProviderBeanDefinition);
122+
123+
BeanDefinitionBuilder registryFactoryBeanBuilder = BeanDefinitionBuilder
124+
.rootBeanDefinition(PluginRegistryFactoryBean.class);
125+
registryFactoryBeanBuilder.addPropertyValue("type", RelProvider.class);
126+
registryFactoryBeanBuilder.addPropertyValue("exclusions", DelegatingRelProvider.class);
127+
128+
AbstractBeanDefinition registryBeanDefinition = registryFactoryBeanBuilder.getBeanDefinition();
129+
registry.registerBeanDefinition("relProviderPluginRegistry", registryBeanDefinition);
130+
131+
BeanDefinitionBuilder delegateBuilder = BeanDefinitionBuilder.rootBeanDefinition(DelegatingRelProvider.class);
132+
delegateBuilder.addConstructorArgValue(registryBeanDefinition);
133+
134+
AbstractBeanDefinition beanDefinition = delegateBuilder.getBeanDefinition();
135+
beanDefinition.setPrimary(true);
136+
registry.registerBeanDefinition(DELEGATING_REL_PROVIDER_BEAN_NAME, beanDefinition);
93137
}
94138

95139
/**
@@ -121,7 +165,17 @@ private AbstractBeanDefinition getLinkDiscovererBeanDefinition(HypermediaType ty
121165
*
122166
* @author Oliver Gierke
123167
*/
124-
private static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor {
168+
private static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
169+
170+
private BeanFactory factory;
171+
172+
/* (non-Javadoc)
173+
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
174+
*/
175+
@Override
176+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
177+
this.factory = beanFactory;
178+
}
125179

126180
/*
127181
* (non-Javadoc)
@@ -164,7 +218,12 @@ private void registerModule(List<HttpMessageConverter<?>> converters) {
164218
}
165219

166220
private void registerModule(Object objectMapper) {
167-
((ObjectMapper) objectMapper).registerModule(new Jackson2HalModule(null));
221+
222+
RelProvider provider = factory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
223+
224+
ObjectMapper mapper = (ObjectMapper) objectMapper;
225+
mapper.registerModule(new Jackson2HalModule());
226+
mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(provider));
168227
}
169228
}
170229

@@ -174,7 +233,18 @@ private void registerModule(Object objectMapper) {
174233
*
175234
* @author Oliver Gierke
176235
*/
177-
private static class Jackson1ModuleRegisteringBeanPostProcessor implements BeanPostProcessor {
236+
private static class Jackson1ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
237+
238+
private BeanFactory beanFactory;
239+
240+
/*
241+
* (non-Javadoc)
242+
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
243+
*/
244+
@Override
245+
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
246+
this.beanFactory = beanFactory;
247+
}
178248

179249
/*
180250
* (non-Javadoc)
@@ -216,8 +286,13 @@ private void registerModule(List<HttpMessageConverter<?>> converters) {
216286
}
217287
}
218288

219-
private void registerModule(Object mapper) {
220-
((org.codehaus.jackson.map.ObjectMapper) mapper).registerModule(new Jackson1HalModule(null));
289+
private void registerModule(Object objectMapper) {
290+
291+
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
292+
293+
org.codehaus.jackson.map.ObjectMapper mapper = (org.codehaus.jackson.map.ObjectMapper) objectMapper;
294+
mapper.registerModule(new Jackson1HalModule());
295+
mapper.setHandlerInstantiator(new Jackson1HalModule.HalHandlerInstantiator(relProvider));
221296
}
222297
}
223298
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.core;
17+
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
21+
import org.springframework.core.annotation.AnnotationUtils;
22+
import org.springframework.core.annotation.Order;
23+
import org.springframework.hateoas.RelProvider;
24+
25+
/**
26+
* @author Oliver Gierke
27+
* @author Alexander Baetz
28+
*/
29+
@Order(100)
30+
public class AnnotationRelProvider implements RelProvider {
31+
32+
private final Map<Class<?>, Relation> annotationCache = new HashMap<Class<?>, Relation>();
33+
34+
/*
35+
* (non-Javadoc)
36+
* @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class)
37+
*/
38+
@Override
39+
public String getCollectionResourceRelFor(Class<?> type) {
40+
41+
Relation annotation = lookupAnnotation(type);
42+
43+
if (annotation == null || Relation.NO_RELATION.equals(annotation.collectionRelation())) {
44+
return null;
45+
}
46+
47+
return annotation.collectionRelation();
48+
}
49+
50+
/*
51+
* (non-Javadoc)
52+
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Object)
53+
*/
54+
@Override
55+
public String getSingleResourceRelFor(Class<?> type) {
56+
57+
Relation annotation = lookupAnnotation(type);
58+
59+
if (annotation == null || Relation.NO_RELATION.equals(annotation.value())) {
60+
return null;
61+
}
62+
63+
return annotation.value();
64+
}
65+
66+
/*
67+
* (non-Javadoc)
68+
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
69+
*/
70+
@Override
71+
public boolean supports(Class<?> delimiter) {
72+
return lookupAnnotation(delimiter) != null;
73+
}
74+
75+
private Relation lookupAnnotation(Class<?> type) {
76+
77+
Relation relation = annotationCache.get(type);
78+
79+
if (relation != null) {
80+
return relation;
81+
}
82+
83+
relation = AnnotationUtils.getAnnotation(type, Relation.class);
84+
85+
if (relation != null) {
86+
annotationCache.put(type, relation);
87+
}
88+
89+
return relation;
90+
}
91+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.core;
17+
18+
import org.springframework.core.Ordered;
19+
import org.springframework.core.annotation.Order;
20+
import org.springframework.hateoas.RelProvider;
21+
import org.springframework.util.StringUtils;
22+
23+
/**
24+
* Default implementation of {@link RelProvider} to simply use the uncapitalized version of the given type's name as
25+
* single resource rel as well as an appended {@code List} for the collection resource rel.
26+
*
27+
* @author Oliver Gierke
28+
*/
29+
@Order(Ordered.LOWEST_PRECEDENCE)
30+
public class DefaultRelProvider implements RelProvider {
31+
32+
/*
33+
* (non-Javadoc)
34+
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
35+
*/
36+
@Override
37+
public boolean supports(Class<?> delimiter) {
38+
return true;
39+
}
40+
41+
/*
42+
* (non-Javadoc)
43+
* @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class)
44+
*/
45+
@Override
46+
public String getCollectionResourceRelFor(Class<?> type) {
47+
return StringUtils.uncapitalize(type.getSimpleName()) + "List";
48+
}
49+
50+
/*
51+
* (non-Javadoc)
52+
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class)
53+
*/
54+
@Override
55+
public String getSingleResourceRelFor(Class<?> type) {
56+
return StringUtils.uncapitalize(type.getSimpleName());
57+
}
58+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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.core;
17+
18+
import org.springframework.hateoas.RelProvider;
19+
import org.springframework.plugin.core.PluginRegistry;
20+
import org.springframework.util.Assert;
21+
22+
/**
23+
* @author Oliver Gierke
24+
*/
25+
public class DelegatingRelProvider implements RelProvider {
26+
27+
private final PluginRegistry<RelProvider, Class<?>> providers;
28+
29+
public DelegatingRelProvider(PluginRegistry<RelProvider, Class<?>> providers) {
30+
31+
Assert.notNull(providers, "RelProviders must not be null!");
32+
this.providers = providers;
33+
}
34+
35+
/*
36+
* (non-Javadoc)
37+
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class)
38+
*/
39+
@Override
40+
public String getSingleResourceRelFor(Class<?> type) {
41+
return providers.getPluginFor(type).getSingleResourceRelFor(type);
42+
}
43+
44+
/*
45+
* (non-Javadoc)
46+
* @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class)
47+
*/
48+
@Override
49+
public String getCollectionResourceRelFor(java.lang.Class<?> type) {
50+
return providers.getPluginFor(type).getCollectionResourceRelFor(type);
51+
}
52+
53+
/*
54+
* (non-Javadoc)
55+
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
56+
*/
57+
@Override
58+
public boolean supports(java.lang.Class<?> delimiter) {
59+
return providers.hasPluginFor(delimiter);
60+
}
61+
}

0 commit comments

Comments
 (0)