diff --git a/src/main/java/org/springframework/hateoas/core/AnnotationAttribute.java b/src/main/java/org/springframework/hateoas/core/AnnotationAttribute.java new file mode 100644 index 00000000..a790349f --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/AnnotationAttribute.java @@ -0,0 +1,67 @@ +/* + * 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.lang.reflect.AnnotatedElement; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; + +/** + * @author Oliver Gierke + */ +public class AnnotationAttribute { + + private final Class annotationType; + private final String attributeName; + + public AnnotationAttribute(Class annotationType) { + this(annotationType, null); + } + + /** + * @param annotationType + * @param attributeName + */ + public AnnotationAttribute(Class annotationType, String attributeName) { + + Assert.notNull(annotationType); + + this.annotationType = annotationType; + this.attributeName = attributeName; + } + + /** + * @return the annotationType + */ + public Class getAnnotationType() { + return annotationType; + } + + public String getValueFrom(AnnotatedElement annotatedElement) { + + Annotation annotation = annotatedElement.getAnnotation(annotationType); + return (String) (attributeName == null ? AnnotationUtils.getValue(annotation) : AnnotationUtils.getValue( + annotation, attributeName)); + } + + public String getValueFrom(Annotation annotation) { + + return (String) (attributeName == null ? AnnotationUtils.getValue(annotation) : AnnotationUtils.getValue( + annotation, attributeName)); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java new file mode 100644 index 00000000..ac844362 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/AnnotationMappingDiscoverer.java @@ -0,0 +1,90 @@ +/* + * 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.springframework.core.annotation.AnnotationUtils.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.springframework.util.Assert; + +/** + * @author Oliver Gierke + */ +public class AnnotationMappingDiscoverer implements MappingDiscoverer { + + private final Class annotationType; + private final String mappingAttributeName; + + public AnnotationMappingDiscoverer(Class annotation) { + this(annotation, null); + } + + public AnnotationMappingDiscoverer(Class annotation, String mappingAttributeName) { + + Assert.notNull(annotation); + + this.annotationType = annotation; + this.mappingAttributeName = mappingAttributeName; + } + + /* (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.Class) + */ + @Override + public String getMapping(Class type) { + + String[] mapping = getMappingFrom(findAnnotation(type, annotationType)); + + if (mapping.length > 1) { + throw new IllegalStateException(String.format("Multiple class level mappings defined on class %s!", + type.getName())); + } + + return mapping[0]; + } + + /* (non-Javadoc) + * @see org.springframework.hateoas.core.MappingDiscoverer#getMapping(java.lang.reflect.Method) + */ + @Override + public String getMapping(Method method) { + + String[] mapping = getMappingFrom(findAnnotation(method, annotationType)); + + if (mapping.length > 1) { + throw new IllegalStateException(String.format("Multiple method level mappings defined on method %s!", + method.toString())); + } + + return getMapping(method.getDeclaringClass()) + mapping[0]; + } + + private String[] getMappingFrom(Annotation annotation) { + + Object value = mappingAttributeName == null ? getValue(annotation) : getValue(annotation, mappingAttributeName); + + if (value instanceof String) { + return new String[] { (String) value }; + } else if (value instanceof String[]) { + return (String[]) value; + } + + throw new IllegalStateException(String.format( + "Unsupported type for the mapping attribute! Support String and String[] but got %s!", value.getClass())); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/LinkBuilderUtils.java b/src/main/java/org/springframework/hateoas/core/LinkBuilderUtils.java new file mode 100644 index 00000000..4378466b --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/LinkBuilderUtils.java @@ -0,0 +1,94 @@ +/* + * 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.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.EmptyTargetSource; +import org.springframework.util.ReflectionUtils; + +/** + * @author Oliver Gierke + */ +public class LinkBuilderUtils { + + public interface LastInvocationAware { + + MethodInvocation getLastInvocation(); + } + + private static class InvocationRecordingMethodInterceptor implements MethodInterceptor, LastInvocationAware { + + private static final Method GET_INVOCATIONS; + private MethodInvocation invocation; + + static { + GET_INVOCATIONS = ReflectionUtils.findMethod(LastInvocationAware.class, "getLastInvocation"); + } + + /* + * (non-Javadoc) + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + + if (GET_INVOCATIONS.equals(invocation.getMethod())) { + return getLastInvocation(); + } else if (Object.class.equals(invocation.getMethod().getDeclaringClass())) { + return invocation.proceed(); + } + + this.invocation = invocation; + + Class returnType = invocation.getMethod().getReturnType(); + + ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); + factory.setTargetClass(returnType); + factory.addInterface(LastInvocationAware.class); + factory.setProxyTargetClass(true); + factory.addAdvice(this); + + return returnType.cast(factory.getProxy()); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.LinkBuilderUtils.LastInvocationAware#getLastInvocation() + */ + @Override + public MethodInvocation getLastInvocation() { + return invocation; + } + } + + @SuppressWarnings("unchecked") + public static T methodOn(Class type) { + + MethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(); + ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); + + factory.setProxyTargetClass(true); + factory.setTargetClass(type); + factory.addInterface(LastInvocationAware.class); + factory.addAdvice(interceptor); + + return (T) factory.getProxy(); + } +} diff --git a/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java b/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java new file mode 100644 index 00000000..bf7cc0d2 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/MappingDiscoverer.java @@ -0,0 +1,30 @@ +/* + * 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.reflect.Method; + +/** + * + * @author Oliver Gierke + */ +public interface MappingDiscoverer { + + String getMapping(Class type); + + String getMapping(Method method); + +} diff --git a/src/main/java/org/springframework/hateoas/core/MethodParameters.java b/src/main/java/org/springframework/hateoas/core/MethodParameters.java new file mode 100644 index 00000000..ff787f43 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/core/MethodParameters.java @@ -0,0 +1,76 @@ +/* + * 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.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; + +/** + * @author Oliver Gierke + */ +public class MethodParameters { + + private final List parameters; + + /** + * Creates a new {@link MethodParameter} from the given {@link Method}. + * + * @param method must not be {@literal null}. + */ + public MethodParameters(Method method) { + + Assert.notNull(method); + this.parameters = new ArrayList(); + + for (int i = 0; i < method.getParameterTypes().length; i++) { + parameters.add(new MethodParameter(method, i)); + } + } + + /** + * Returns all {@link MethodParameter}s. + * + * @return + */ + public List getParameters() { + return parameters; + } + + /** + * Returns all {@link MethodParameter}s annotated with the given annotation type. + * + * @param annotation must not be {@literal null}. + * @return + */ + public List getParametersWith(Class annotation) { + + Assert.notNull(annotation); + List result = new ArrayList(); + + for (MethodParameter parameter : getParameters()) { + if (parameter.hasParameterAnnotation(annotation)) { + result.add(parameter); + } + } + + return result; + } +} diff --git a/src/main/java/org/springframework/hateoas/jaxrs/JaxRsLinkBuilder.java b/src/main/java/org/springframework/hateoas/jaxrs/JaxRsLinkBuilder.java index 9352586f..f5ab0611 100644 --- a/src/main/java/org/springframework/hateoas/jaxrs/JaxRsLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/jaxrs/JaxRsLinkBuilder.java @@ -17,9 +17,8 @@ package org.springframework.hateoas.jaxrs; import javax.ws.rs.Path; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.hateoas.LinkBuilder; -import org.springframework.hateoas.core.LinkBuilderSupport; +import org.springframework.hateoas.mvc.UriComponentsLinkBuilder; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriTemplate; @@ -29,7 +28,7 @@ import org.springframework.web.util.UriTemplate; * * @author Oliver Gierke */ -public class JaxRsLinkBuilder extends LinkBuilderSupport { +public class JaxRsLinkBuilder extends UriComponentsLinkBuilder { /** * Creates a new {@link JaxRsLinkBuilder} from the given {@link UriComponentsBuilder}. @@ -61,12 +60,9 @@ public class JaxRsLinkBuilder extends LinkBuilderSupport { */ public static JaxRsLinkBuilder linkTo(Class service, Object... parameters) { - Path annotation = AnnotationUtils.findAnnotation(service, Path.class); - String path = (String) AnnotationUtils.getValue(annotation); - JaxRsLinkBuilder builder = new JaxRsLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()); - UriTemplate template = new UriTemplate(path); + UriTemplate template = new UriTemplate(DISCOVERER.getMapping(service)); return builder.slash(template.expand(parameters)); } diff --git a/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java b/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java new file mode 100644 index 00000000..dfdbc532 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/AnnotatedParametersParameterAccessor.java @@ -0,0 +1,59 @@ +/* + * 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 java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.MethodParameter; +import org.springframework.hateoas.core.AnnotationAttribute; +import org.springframework.hateoas.core.MethodParameters; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Oliver Gierke + */ +public class AnnotatedParametersParameterAccessor { + + private final AnnotationAttribute attribute; + + public AnnotatedParametersParameterAccessor(AnnotationAttribute attribute) { + + Assert.notNull(attribute); + this.attribute = attribute; + } + + public Map getBoundParameters(MethodInvocation invocation) { + + MethodParameters parameters = new MethodParameters(invocation.getMethod()); + Object[] arguments = invocation.getArguments(); + Map result = new HashMap(); + + for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) { + + Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType()); + String annotationAttributeValue = attribute.getValueFrom(annotation); + String key = StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue : parameter + .getParameterName(); + result.put(key, arguments[parameter.getParameterIndex()]); + } + + return result; + } +} diff --git a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java index f595a1cb..d3b1f390 100755 --- a/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java +++ b/src/main/java/org/springframework/hateoas/mvc/ControllerLinkBuilder.java @@ -15,10 +15,13 @@ */ package org.springframework.hateoas.mvc; -import org.springframework.core.annotation.AnnotationUtils; +import java.lang.reflect.Method; +import java.net.URI; + +import org.aopalliance.intercept.MethodInvocation; import org.springframework.hateoas.Link; -import org.springframework.hateoas.core.LinkBuilderSupport; import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; @@ -29,7 +32,7 @@ import org.springframework.web.util.UriTemplate; * * @author Oliver Gierke */ -public class ControllerLinkBuilder extends LinkBuilderSupport { +public class ControllerLinkBuilder extends UriComponentsLinkBuilder { /** * Creates a new {@link ControllerLinkBuilder} using the given {@link UriComponentsBuilder}. @@ -63,21 +66,28 @@ public class ControllerLinkBuilder extends LinkBuilderSupport 1) { - throw new IllegalStateException("Multiple controller mappings defined! Unable to build URI!"); - } - ControllerLinkBuilder builder = new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()); - if (mapping.length == 0) { - return builder; + UriTemplate template = new UriTemplate(DISCOVERER.getMapping(controller)); + return builder.slash(template.expand(parameters)); } - UriTemplate template = new UriTemplate(mapping[0]); - return builder.slash(template.expand(parameters)); + public static ControllerLinkBuilder linkTo(Object invocationValue) { + + Assert.isInstanceOf(LastInvocationAware.class, invocationValue); + LastInvocationAware invocations = (LastInvocationAware) invocationValue; + + MethodInvocation invocation = invocations.getLastInvocation(); + Method method = invocation.getMethod(); + + UriTemplate template = new UriTemplate(DISCOVERER.getMapping(method)); + URI uri = template.expand(accessor.getBoundParameters(invocation)); + + return new ControllerLinkBuilder(ServletUriComponentsBuilder.fromCurrentServletMapping()).slash(uri); + } + + public static T methodOn(Class controller) { + return LinkBuilderUtils.methodOn(controller); } /* diff --git a/src/test/java/org/springframework/hateoas/mvc/LinkBuilderUtilsUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/LinkBuilderUtilsUnitTest.java new file mode 100644 index 00000000..9bf6e883 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mvc/LinkBuilderUtilsUnitTest.java @@ -0,0 +1,49 @@ +/* + * 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.junit.Test; +import org.springframework.hateoas.TestUtils; +import org.springframework.hateoas.core.LinkBuilderUtils; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author Oliver Gierke + */ +public class LinkBuilderUtilsUnitTest extends TestUtils { + + @Test + public void test() { + + ControllerLinkBuilder builder = ControllerLinkBuilder.linkTo(LinkBuilderUtils.methodOn(SampleController.class) + .someMethod(1L)); + System.out.println(builder.toUri()); + + } + + @RequestMapping("/sample") + static class SampleController { + + @RequestMapping("/{id}/foo") + HttpEntity someMethod(@PathVariable("id") Long id) { + return new ResponseEntity(HttpStatus.OK); + } + } +}