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 extends Annotation> type,
+ Class extends LinkBuilderFactory>> 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:
+ *
+ * - 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.
+ * - Individual resources are exposed via a nested mapping consisting of the id of the managed entity, e.g. {@code
+ * @RequestMapping("/{id}")}.
-
+ *
+ *
+ * @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 extends LinkBuilder> 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 extends Class>> controllerTypes,
+ LinkBuilderFactory extends LinkBuilder> 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 extends Annotation> annotation;
+ private LinkBuilderFactory extends LinkBuilder> 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 extends Annotation> 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 extends LinkBuilder> 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 extends Annotation> 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 {
+
+ }
+}