From 04be1871c3ab9f7951d1d4d37bb4d28e33a14ca6 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 29 Oct 2012 15:20:10 +0100 Subject: [PATCH] Introduced EntityLinks abstraction. EntityLinks allow accessing LinkBuilders or even Link instance based on components exposing REST resources for a particular domain type. The core implementation is ControllerEntityLinks that assumes a particular URI structure so that it can derive the URIs to be asked for per domain type. The domain type managed will be inspected from the @ExposesResourceFor annotation on a controller class. We provide a factory bean to allow configuring instances of ControllerEntityLinks based on a mapping annotation and LinkBuilderFactory. Added infrastructure to activate automatic discovery of EntityLinks implementations through @EnableEntityLinks. This will automatically register ControllerEntityLinks instances for Spring MVC controllers annotated with @ExposesResourceFor as well as JAX-RS resources if JAX-RS is on the classpath. The mechanism essentially registers a DelegatingEntityLinks instance leveraging the Spring Plugin PluginRegistry mechanism to delegate to the actual EntityLinks instance actually registered for the entity type. It will become the primary EntityLinks bean in the ApplicationContext so that they can be autowired into client components safely. --- pom.xml | 23 ++- .../springframework/hateoas/EntityLinks.java | 91 ++++++++++++ .../hateoas/ExposesResourceFor.java | 45 ++++++ .../hateoas/config/EnableEntityLinks.java | 38 +++++ .../LinkBuilderBeanDefinitionRegistrar.java | 100 +++++++++++++ .../hateoas/core/AbstractEntityLinks.java | 57 ++++++++ .../hateoas/core/ControllerEntityLinks.java | 138 ++++++++++++++++++ .../ControllerEntityLinksFactoryBean.java | 127 ++++++++++++++++ .../hateoas/core/DelegatingEntityLinks.java | 108 ++++++++++++++ .../hateoas/mvc/BasicLinkBuilder.java | 65 +++++++++ .../hateoas/config/ConfigIntegrationTest.java | 92 ++++++++++++ ...trollerEntityLinksFactoryBeanUnitTest.java | 70 +++++++++ .../core/ControllerEntityLinksUnitTest.java | 101 +++++++++++++ .../core/DelegatingEntityLinksUnitTest.java | 101 +++++++++++++ 14 files changed, 1155 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/hateoas/EntityLinks.java create mode 100644 src/main/java/org/springframework/hateoas/ExposesResourceFor.java create mode 100644 src/main/java/org/springframework/hateoas/config/EnableEntityLinks.java create mode 100644 src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java create mode 100644 src/main/java/org/springframework/hateoas/core/AbstractEntityLinks.java create mode 100644 src/main/java/org/springframework/hateoas/core/ControllerEntityLinks.java create mode 100644 src/main/java/org/springframework/hateoas/core/ControllerEntityLinksFactoryBean.java create mode 100644 src/main/java/org/springframework/hateoas/core/DelegatingEntityLinks.java create mode 100644 src/main/java/org/springframework/hateoas/mvc/BasicLinkBuilder.java create mode 100644 src/test/java/org/springframework/hateoas/config/ConfigIntegrationTest.java create mode 100644 src/test/java/org/springframework/hateoas/core/ControllerEntityLinksFactoryBeanUnitTest.java create mode 100644 src/test/java/org/springframework/hateoas/core/ControllerEntityLinksUnitTest.java create mode 100644 src/test/java/org/springframework/hateoas/core/DelegatingEntityLinksUnitTest.java diff --git a/pom.xml b/pom.xml index 2d860719..a76e3642 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,20 @@ ${spring.version} + + org.springframework.plugin + spring-plugin-core + 0.8.0.BUILD-SNAPSHOT + true + + + + cglib + cglib + 2.2.2 + true + + javax.servlet servlet-api @@ -225,7 +239,14 @@ - + + + spring-snapshots + http://repo.springsource.org/libs-snapshot + + + + https://github.com/SpringSource/spring-hateoas scm:git:git://github.com/SpringSource/spring-hateoas.git scm:git:ssh://git@github.com:SpringSource/spring-hateoas.git diff --git a/src/main/java/org/springframework/hateoas/EntityLinks.java b/src/main/java/org/springframework/hateoas/EntityLinks.java new file mode 100644 index 00000000..743f219e --- /dev/null +++ b/src/main/java/org/springframework/hateoas/EntityLinks.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012 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; + +import org.springframework.plugin.core.Plugin; + +/** + * Accessor to links pointing to controllers backing an entity type. + * + * @author Oliver Gierke + */ +public interface EntityLinks extends Plugin> { + + /** + * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity type. Expects a + * controller being mapped to a fully expanded URI template (i.e. not path variables being used). + * + * @param type the entity type to point to, must not be {@literal null}. + * @return the {@link LinkBuilder} pointing to the collection resource. Will never be {@literal null}. + */ + LinkBuilder linkFor(Class type); + + /** + * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity type, unfolding the + * given parameters into the URI template the backing controller is mapped to. + * + * @param type the entity type to point to, must not be {@literal null}. + * @return the {@link LinkBuilder} pointing to the collection resource. Will never be {@literal null}. + */ + LinkBuilder linkFor(Class type, Object... parameters); + + /** + * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity type and id. + * Implementations will know about the URI structure being used to expose single-resource URIs. + * + * @param type the entity type to point to, must not be {@literal null}. + * @param id the id of the object of the handed type, {@link Identifiable}s will be unwrapped. + * @return + */ + LinkBuilder linkForSingleResource(Class type, Object id); + + /** + * Returns a {@link LinkBuilder} able to create links to the controller managing the given entity. + * + * @see #linkForSingleResource(Class, Object) + * @param entity the entity type to point to, must not be {@literal null}. + * @return + */ + LinkBuilder linkForSingleResource(Identifiable entity); + + /** + * Creates a {@link Link} pointing to the collection resource of the given type. The relation type of the link will be + * determined by the implementation class and should be defaulted to {@link Link#REL_SELF}. + * + * @param type the entity type to point to, must not be {@literal null}. + * @return + */ + Link linkToCollectionResource(Class type); + + /** + * Creates a {@link Link} pointing to single resource backing the given entity type and id. The relation type of the + * link will be determined by the implementation class and should be defaulted to {@link Link#REL_SELF}. + * + * @param type the entity type to point to, must not be {@literal null}. + * @param id + * @return + */ + Link linkToSingleResource(Class type, Object id); + + /** + * Creates a {@link Link} pointing to single resource backing the given entity. The relation type of the link will be + * determined by the implementation class and should be defaulted to {@link Link#REL_SELF}. + * + * @param entity the entity type to point to, must not be {@literal null}. + * @return + */ + Link linkToSingleResource(Identifiable entity); +} diff --git a/src/main/java/org/springframework/hateoas/ExposesResourceFor.java b/src/main/java/org/springframework/hateoas/ExposesResourceFor.java new file mode 100644 index 00000000..2ae92243 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/ExposesResourceFor.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012 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; + +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.hateoas.core.ControllerEntityLinks; + +/** + * Annotation to demarcate controllers that expose URI templates of a structure according to + * {@link ControllerEntityLinks}. + * + * @author Oliver Gierke + */ +@Inherited +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ExposesResourceFor { + + /** + * The domain type the controller exposes resources for. + * + * @return + */ + Class value(); +} diff --git a/src/main/java/org/springframework/hateoas/config/EnableEntityLinks.java b/src/main/java/org/springframework/hateoas/config/EnableEntityLinks.java new file mode 100644 index 00000000..546096d7 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/EnableEntityLinks.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012 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.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; +import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.core.DelegatingEntityLinks; + +/** + * Enables the collection of {@link LinkBuilder} instances from the application context. All found ones will be exposed + * through an instance of {@link DelegatingEntityLinks}. + * + * @author Oliver Gierke + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Import(LinkBuilderBeanDefinitionRegistrar.class) +public @interface EnableEntityLinks { + +} diff --git a/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java new file mode 100644 index 00000000..dc51a4ab --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012 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.Annotation; + +import javax.ws.rs.Path; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.LinkBuilderFactory; +import org.springframework.hateoas.core.ControllerEntityLinksFactoryBean; +import org.springframework.hateoas.core.DelegatingEntityLinks; +import org.springframework.hateoas.jaxrs.JaxRsLinkBuilderFactory; +import org.springframework.hateoas.mvc.ControllerLinkBuilderFactory; +import org.springframework.plugin.core.support.PluginRegistryFactoryBean; +import org.springframework.stereotype.Controller; +import org.springframework.util.ClassUtils; + +/** + * {@link ImportBeanDefinitionRegistrar} to register a {@link DelegatingEntityLinks} instance as well as a + * {@link ControllerEntityLinksFactoryBean} for Spring MVC controllers and JAX-RS resources if present. + * + * @author Oliver Gierke + */ +public class LinkBuilderBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { + + private static final boolean IS_JAX_RS_PRESENT = ClassUtils.isPresent("javax.ws.rs.Path", + ClassUtils.getDefaultClassLoader()); + + /* + * (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) { + + BeanDefinitionBuilder registryFactoryBeanBuilder = BeanDefinitionBuilder + .rootBeanDefinition(PluginRegistryFactoryBean.class); + registryFactoryBeanBuilder.addPropertyValue("type", EntityLinks.class); + registryFactoryBeanBuilder.addPropertyValue("exclusions", DelegatingEntityLinks.class); + + AbstractBeanDefinition registryBeanDefinition = registryFactoryBeanBuilder.getBeanDefinition(); + registry.registerBeanDefinition("entityLinksPluginRegistry", registryBeanDefinition); + + BeanDefinitionBuilder delegateBuilder = BeanDefinitionBuilder.rootBeanDefinition(DelegatingEntityLinks.class); + delegateBuilder.addConstructorArgValue(registryBeanDefinition); + + BeanDefinitionBuilder builder = getEntityControllerLinksFor(Controller.class, ControllerLinkBuilderFactory.class); + registry.registerBeanDefinition("controllerEntityLinks", builder.getBeanDefinition()); + delegateBuilder.addDependsOn("controllerEntityLinks"); + + if (IS_JAX_RS_PRESENT) { + JaxRsEntityControllerBuilderDefinitionBuilder definitionBuilder = new JaxRsEntityControllerBuilderDefinitionBuilder(); + registry.registerBeanDefinition("jaxRsEntityLinks", definitionBuilder.getBeanDefinition()); + delegateBuilder.addDependsOn("jaxRsEntityLinks"); + } + + AbstractBeanDefinition beanDefinition = delegateBuilder.getBeanDefinition(); + beanDefinition.setPrimary(true); + registry.registerBeanDefinition("delegatingEntityLinks", beanDefinition); + } + + private static BeanDefinitionBuilder getEntityControllerLinksFor(Class type, + Class> linkBuilderFactoryType) { + + BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ControllerEntityLinksFactoryBean.class); + builder.addPropertyValue("annotation", type); + builder.addPropertyValue("linkBuilderFactory", new RootBeanDefinition(linkBuilderFactoryType)); + + return builder; + } + + static class JaxRsEntityControllerBuilderDefinitionBuilder { + + public BeanDefinition getBeanDefinition() { + BeanDefinitionBuilder builder = getEntityControllerLinksFor(Path.class, JaxRsLinkBuilderFactory.class); + return builder.getBeanDefinition(); + } + } +} diff --git a/src/main/java/org/springframework/hateoas/core/AbstractEntityLinks.java b/src/main/java/org/springframework/hateoas/core/AbstractEntityLinks.java new file mode 100644 index 00000000..58112be2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/AbstractEntityLinks.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012 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.core; + +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Identifiable; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkBuilder; + +/** + * Implementation base class to delegate the higher level methods of {@link EntityLinks} by delegating to the more fine + * grained ones to reduce the implementation effort for actual implementation classes. + * + * @author Oliver Gierke + */ +public abstract class AbstractEntityLinks implements EntityLinks { + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#getLinkToSingleResource(org.springframework.hateoas.Identifiable) + */ + @Override + public Link linkToSingleResource(Identifiable entity) { + return linkToSingleResource(entity.getClass(), entity.getId()); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkForSingleResource(java.lang.Class, java.lang.Object) + */ + @Override + public LinkBuilder linkForSingleResource(Class type, Object id) { + return linkFor(type).slash(id); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkForSingleResource(org.springframework.hateoas.Identifiable) + */ + @Override + public LinkBuilder linkForSingleResource(Identifiable entity) { + return linkForSingleResource(entity.getClass(), entity.getId()); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/ControllerEntityLinks.java b/src/main/java/org/springframework/hateoas/core/ControllerEntityLinks.java new file mode 100644 index 00000000..0c959373 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/ControllerEntityLinks.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012 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.core; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.LinkBuilderFactory; +import org.springframework.hateoas.ExposesResourceFor; +import org.springframework.util.Assert; + +/** + * {@link EntityLinks} implementation which assumes a certain URI mapping structure: + *
    + *
  1. A class-level mapping annotation that can contain template variables. The URI needs to expose the collection + * resource, which means the controller has to expose a handler method mapped to an empty path: e.g. + * {@code @RequestMapping(method = RequestMethod.GET)} in case of a Spring MVC controller.
  2. + *
  3. Individual resources are exposed via a nested mapping consisting of the id of the managed entity, e.g. {@code + * @RequestMapping("/{id}")}.
  4. + *
+ * + * @Controller + * @ExposesResourceFor(Order.class) + * @RequestMapping("/orders") + * class OrderController { + * + * @RequestMapping + * ResponseEntity orders(…) { … } + * + * @RequestMapping("/{id}") + * ResponseEntity order(@PathVariable("id") … ) { … } + * } + * + * + * @author Oliver Gierke + */ +public class ControllerEntityLinks extends AbstractEntityLinks { + + private final Map, Class> entityToController; + private final LinkBuilderFactory linkBuilderFactory; + + /** + * Creates a new {@link ControllerEntityLinks} inspecting the configured classes for the given annotation. + * + * @param controllerTypes the controller classes to be inspected. + * @param linkBuilderFactory the {@link LinkBuilder} to use to create links. + */ + public ControllerEntityLinks(Iterable> controllerTypes, + LinkBuilderFactory linkBuilderFactory) { + + Assert.notNull(controllerTypes); + Assert.notNull(linkBuilderFactory); + + this.linkBuilderFactory = linkBuilderFactory; + this.entityToController = new HashMap, Class>(); + + for (Class controllerType : controllerTypes) { + registerControllerClass(controllerType); + } + } + + private void registerControllerClass(Class controllerType) { + + Assert.notNull(controllerType, "Controller type must nor be null!"); + ExposesResourceFor annotation = AnnotationUtils.findAnnotation(controllerType, ExposesResourceFor.class); + + if (annotation != null) { + entityToController.put(annotation.value(), controllerType); + } else { + throw new IllegalArgumentException(String.format("Controller %s must be annotated with @ExposesResourceFor!", + controllerType.getName())); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkTo(java.lang.Class) + */ + @Override + public LinkBuilder linkFor(Class entity) { + + Class controllerClass = entityToController.get(entity); + return linkBuilderFactory.linkTo(controllerClass); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkTo(java.lang.Class, java.lang.Object) + */ + @Override + public LinkBuilder linkFor(Class entity, Object... parameters) { + return linkFor(entity, parameters); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#getLinkToCollectionResource(java.lang.Class) + */ + @Override + public Link linkToCollectionResource(Class entity) { + return linkFor(entity).withSelfRel(); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#getLinkToSingleResource(java.lang.Class, java.lang.Object) + */ + @Override + public Link linkToSingleResource(Class entity, Object id) { + return linkFor(entity).slash(id).withSelfRel(); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return entityToController.containsKey(delimiter); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/ControllerEntityLinksFactoryBean.java b/src/main/java/org/springframework/hateoas/core/ControllerEntityLinksFactoryBean.java new file mode 100644 index 00000000..6f5cf671 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/ControllerEntityLinksFactoryBean.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012 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.core; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.hateoas.ExposesResourceFor; +import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.LinkBuilderFactory; +import org.springframework.util.Assert; + +/** + * {@link FactoryBean} implementation to create {@link ControllerEntityLinks} instances looking up controller classes + * from an {@link ApplicationContext}. The controller types are identified by the annotation type configured. + * + * @author Oliver Gierke + */ +public class ControllerEntityLinksFactoryBean extends AbstractFactoryBean implements + ApplicationContextAware { + + private Class annotation; + private LinkBuilderFactory linkBuilderFactory; + private ApplicationContext context; + + /** + * Configures the annotation type to inspect the {@link ApplicationContext} for beans that carry the given annotation. + * + * @param annotation must not be {@literal null}. + */ + public void setAnnotation(Class annotation) { + Assert.notNull(annotation); + this.annotation = annotation; + } + + /** + * Configures the {@link LinkBuilderFactory} to be used to create {@link LinkBuilder} instances. + * + * @param linkBuilderFactory the linkBuilderFactory to set + */ + public void setLinkBuilderFactory(LinkBuilderFactory linkBuilderFactory) { + this.linkBuilderFactory = linkBuilderFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext context) { + this.context = context; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.AbstractFactoryBean#getObjectType() + */ + @Override + public Class getObjectType() { + return ControllerEntityLinks.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance() + */ + @Override + protected ControllerEntityLinks createInstance() throws Exception { + + Collection> controllerTypes = new HashSet>(); + + for (Class controllerType : getBeanTypesWithAnnotation(annotation)) { + if (AnnotationUtils.findAnnotation(controllerType, ExposesResourceFor.class) != null) { + controllerTypes.add(controllerType); + } + } + + return new ControllerEntityLinks(controllerTypes, linkBuilderFactory); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.AbstractFactoryBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() throws Exception { + + Assert.state(annotation != null, "Annotation type must be configured!"); + Assert.state(linkBuilderFactory != null, "LinkBuilderFactory must be configured!"); + super.afterPropertiesSet(); + } + + private Iterable> getBeanTypesWithAnnotation(Class type) { + + Set> annotatedTypes = new HashSet>(); + + for (String beanName : context.getBeanDefinitionNames()) { + + Annotation annotation = context.findAnnotationOnBean(beanName, type); + if (annotation != null) { + annotatedTypes.add(context.getType(beanName)); + } + } + + return annotatedTypes; + } +} diff --git a/src/main/java/org/springframework/hateoas/core/DelegatingEntityLinks.java b/src/main/java/org/springframework/hateoas/core/DelegatingEntityLinks.java new file mode 100644 index 00000000..e21ef110 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/DelegatingEntityLinks.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012 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.core; + +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkBuilder; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.util.Assert; + +/** + * {@link EntityLinks} implementation that delegates to the {@link EntityLinks} instances registered in the + * {@link PluginRegistry} given on instance creation. + * + * @author Oliver Gierke + */ +public class DelegatingEntityLinks extends AbstractEntityLinks { + + private final PluginRegistry> delegates; + + /** + * Creates a new {@link DelegatingEntityLinks} using the given {@link PluginRegistry}. + * + * @param plugins must not be {@literal null}. + */ + public DelegatingEntityLinks(PluginRegistry> plugins) { + + Assert.notNull(plugins, "PluginRegistry must not be null!"); + this.delegates = plugins; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkFor(java.lang.Class) + */ + @Override + public LinkBuilder linkFor(Class type) { + return getPluginFor(type).linkFor(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkFor(java.lang.Class, java.lang.Object[]) + */ + @Override + public LinkBuilder linkFor(Class type, Object... parameters) { + return getPluginFor(type).linkFor(type, parameters); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#getLinkToCollectionResource(java.lang.Class) + */ + @Override + public Link linkToCollectionResource(Class type) { + return getPluginFor(type).linkToCollectionResource(type); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#getLinkToSingleResource(java.lang.Class, java.lang.Object) + */ + @Override + public Link linkToSingleResource(Class type, Object id) { + return getPluginFor(type).linkToSingleResource(type, id); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return delegates.hasPluginFor(delimiter); + } + + /** + * Returns the plugin for the given type or throws an {@link IllegalStateException} if no delegate {@link EntityLinks} + * can be found. + * + * @param type must not be {@literal null}. + * @return + */ + private EntityLinks getPluginFor(Class type) { + + EntityLinks plugin = delegates.getPluginFor(type); + + if (plugin == null) { + throw new IllegalStateException(String.format( + "Cannot determine link for %s! No EntityLinks instance found supporting the domain type!", type.getName())); + } + + return plugin; + } +} diff --git a/src/main/java/org/springframework/hateoas/mvc/BasicLinkBuilder.java b/src/main/java/org/springframework/hateoas/mvc/BasicLinkBuilder.java new file mode 100644 index 00000000..9913a5d5 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/BasicLinkBuilder.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012 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.mvc; + +import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.core.LinkBuilderSupport; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Simples {@link LinkBuilder} implementation possible. Exposes a link to the current servlet mapping only. + * + * @author Oliver Gierke + */ +public class BasicLinkBuilder extends LinkBuilderSupport { + + /** + * Creates a new {@link BasicLinkBuilder} using the given {@link UriComponentsBuilder}. + * + * @param builder must not be {@literal null}. + */ + private BasicLinkBuilder(UriComponentsBuilder builder) { + super(builder); + } + + /** + * Creates a new {@link BasicLinkBuilder} to link to the current servlet mapping. + * + * @return + */ + public static BasicLinkBuilder linkToCurrentMapping() { + return new BasicLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mvc.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder) + */ + @Override + protected BasicLinkBuilder createNewInstance(UriComponentsBuilder builder) { + return new BasicLinkBuilder(builder); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mvc.LinkBuilderSupport#getThis() + */ + @Override + protected BasicLinkBuilder getThis() { + return this; + } +} diff --git a/src/test/java/org/springframework/hateoas/config/ConfigIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/ConfigIntegrationTest.java new file mode 100644 index 00000000..4468497a --- /dev/null +++ b/src/test/java/org/springframework/hateoas/config/ConfigIntegrationTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012 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.CoreMatchers.*; +import static org.junit.Assert.*; + +import javax.ws.rs.Path; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.ExposesResourceFor; +import org.springframework.hateoas.core.DelegatingEntityLinks; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Integration test for {@link EnableEntityLinks} annotation. + * + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class ConfigIntegrationTest { + + @Configuration + @EnableEntityLinks + static class Config { + + @Bean + public SampleController controller() { + return new SampleController(); + } + + @Bean + public SampleResource resource() { + return new SampleResource(); + } + } + + @Autowired + DelegatingEntityLinks builder; + + @Test + public void initializesDelegatingEntityLinks() { + + assertThat(builder, is(notNullValue())); + assertThat(builder.supports(Person.class), is(true)); + assertThat(builder.supports(Address.class), is(true)); + assertThat(builder.supports(Object.class), is(false)); + } + + @Controller + @ExposesResourceFor(Person.class) + @RequestMapping("/person") + static class SampleController { + + } + + @Path("/address") + @ExposesResourceFor(Address.class) + static class SampleResource { + + } + + static class Person { + + } + + static class Address { + + } + +} diff --git a/src/test/java/org/springframework/hateoas/core/ControllerEntityLinksFactoryBeanUnitTest.java b/src/test/java/org/springframework/hateoas/core/ControllerEntityLinksFactoryBeanUnitTest.java new file mode 100644 index 00000000..3e243570 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/core/ControllerEntityLinksFactoryBeanUnitTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012 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.core; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.hateoas.core.ControllerEntityLinksUnitTest.Person; +import org.springframework.hateoas.core.ControllerEntityLinksUnitTest.SampleController; +import org.springframework.hateoas.mvc.ControllerLinkBuilderFactory; +import org.springframework.stereotype.Controller; + +/** + * Unit tests for {@link ControllerEntityLinksFactoryBean}. + * + * @author Oliver Gierke + */ +public class ControllerEntityLinksFactoryBeanUnitTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void rejectsFactoryBeanIfAnnotationNotSet() throws Exception { + + exception.expect(IllegalStateException.class); + exception.expectMessage("Annotation"); + + ControllerEntityLinksFactoryBean builder = new ControllerEntityLinksFactoryBean(); + builder.afterPropertiesSet(); + } + + @Test + public void discoversSampleControllerFromApplicationContext() throws Exception { + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("controller", new RootBeanDefinition(SampleController.class)); + + ApplicationContext context = new GenericApplicationContext(factory); + + ControllerEntityLinksFactoryBean builder = new ControllerEntityLinksFactoryBean(); + builder.setAnnotation(Controller.class); + builder.setLinkBuilderFactory(new ControllerLinkBuilderFactory()); + builder.setApplicationContext(context); + builder.afterPropertiesSet(); + + ControllerEntityLinks entityLinks = builder.getObject(); + assertThat(entityLinks.supports(Person.class), is(true)); + } +} diff --git a/src/test/java/org/springframework/hateoas/core/ControllerEntityLinksUnitTest.java b/src/test/java/org/springframework/hateoas/core/ControllerEntityLinksUnitTest.java new file mode 100644 index 00000000..0dcf381d --- /dev/null +++ b/src/test/java/org/springframework/hateoas/core/ControllerEntityLinksUnitTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012 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.core; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.LinkBuilderFactory; +import org.springframework.hateoas.ExposesResourceFor; +import org.springframework.hateoas.TestUtils; +import org.springframework.hateoas.mvc.ControllerLinkBuilder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Unit tests for {@link ControllerEntityLinks}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class ControllerEntityLinksUnitTest extends TestUtils { + + @Mock + LinkBuilderFactory linkBuilderFactory; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + @SuppressWarnings("unchecked") + public void rejectsUnannotatedController() { + + thrown.expectMessage(InvalidController.class.getName()); + new ControllerEntityLinks(Arrays.asList(InvalidController.class), linkBuilderFactory); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullControllerList() { + + new ControllerEntityLinks(null, linkBuilderFactory); + } + + @Test + @SuppressWarnings("unchecked") + public void rejectsNullLinkBuilderFactory() { + + thrown.expectMessage(InvalidController.class.getName()); + new ControllerEntityLinks(Arrays.asList(InvalidController.class), linkBuilderFactory); + } + + @Test + @SuppressWarnings("unchecked") + public void registersControllerForEntity() { + + when(linkBuilderFactory.linkTo(SampleController.class)).thenReturn( + ControllerLinkBuilder.linkTo(SampleController.class)); + EntityLinks links = new ControllerEntityLinks(Arrays.asList(SampleController.class), linkBuilderFactory); + + assertThat(links.supports(Person.class), is(true)); + assertThat(links.linkFor(Person.class), is(notNullValue())); + } + + @Controller + @ExposesResourceFor(Person.class) + @RequestMapping("/person") + static class SampleController { + + } + + static class InvalidController { + + } + + static class Person { + + } +} diff --git a/src/test/java/org/springframework/hateoas/core/DelegatingEntityLinksUnitTest.java b/src/test/java/org/springframework/hateoas/core/DelegatingEntityLinksUnitTest.java new file mode 100644 index 00000000..ae20cb95 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/core/DelegatingEntityLinksUnitTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012 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.core; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.ExposesResourceFor; +import org.springframework.hateoas.TestUtils; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.plugin.core.SimplePluginRegistry; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Unit tests for {@link DelegatingEntityLinks}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingEntityLinksUnitTest extends TestUtils { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Mock + EntityLinks target; + + @Before + @Override + public void setUp() { + when(target.supports(String.class)).thenReturn(true); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullPluginRegistry() { + new DelegatingEntityLinks(null); + } + + @Test + public void throwsExceptionForUnsupportedClass() { + + exception.expect(IllegalStateException.class); + exception.expectMessage(String.class.getName()); + + EntityLinks links = new DelegatingEntityLinks(SimplePluginRegistry., EntityLinks> create()); + links.linkFor(String.class); + } + + @Test + public void supportsDomainTypeBackedByPlugin() { + + EntityLinks links = createDelegatingEntityLinks(); + + assertThat(links.supports(String.class), is(true)); + } + + @Test + public void delegatesLinkForCall() { + + EntityLinks links = createDelegatingEntityLinks(); + + links.linkFor(String.class); + verify(target, times(1)).linkFor(String.class); + } + + private EntityLinks createDelegatingEntityLinks() { + + PluginRegistry> registry = SimplePluginRegistry.create(Arrays.asList(target)); + return new DelegatingEntityLinks(registry); + } + + @ExposesResourceFor(String.class) + @RequestMapping("/string") + static class Controller { + + } +}