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 { + + } +}