#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).
This commit is contained in:
Oliver Gierke
2013-02-15 11:21:23 +01:00
parent 35ad18839e
commit 3976ae168b
6 changed files with 351 additions and 3 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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());