diff --git a/build.gradle b/build.gradle index 9e34ef309..ab093e1f2 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ ext { hateoasVersion = "0.9.0.BUILD-SNAPSHOT" springPluginVersion = "0.8.0.RELEASE" springSecurityVersion = "3.1.3.RELEASE" - sdCommonsVersion = "1.7.0.M1" + sdCommonsVersion = "1.7.0.BUILD-SNAPSHOT" sdJpaVersion = "1.5.0.M1" sdMongoVersion = "1.4.0.M1" sdGemfireVersion = "1.3.3.RELEASE" diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java index fa44f1b21..c33242d87 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/MethodResourceMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 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. @@ -16,6 +16,7 @@ package org.springframework.data.rest.core.mapping; import java.lang.reflect.Method; +import java.util.List; /** * A {@link ResourceMapping} that is backed by a {@link Method}. @@ -30,4 +31,11 @@ public interface MethodResourceMapping extends ResourceMapping { * @return */ Method getMethod(); + + /** + * Returns the names of the parameters the method exposes. + * + * @return + */ + List getParameterNames(); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java index c350d4184..16ec0c08a 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 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. @@ -16,10 +16,17 @@ package org.springframework.data.rest.core.mapping; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.hateoas.core.AnnotationAttribute; +import org.springframework.hateoas.core.MethodParameters; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -30,11 +37,15 @@ import org.springframework.util.StringUtils; */ class RepositoryMethodResourceMapping implements MethodResourceMapping { + private static final AnnotationAttribute PARAM_VALUE = new AnnotationAttribute(Param.class); + private final boolean isExported; private final String rel; private final Path path; private final Method method; + private final List parameterNames; + /** * Creates a new {@link RepositoryMethodResourceMapping} for the given {@link Method}. * @@ -53,6 +64,21 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { this.path = annotation == null || !StringUtils.hasText(annotation.path()) ? new Path(method.getName()) : new Path( annotation.path()); this.method = method; + this.parameterNames = discoverParameterNames(method); + } + + private static final List discoverParameterNames(Method method) { + + List result = new ArrayList(); + + for (MethodParameter parameter : new MethodParameters(method, PARAM_VALUE).getParameters()) { + String name = parameter.getParameterName(); + if (name != null) { + result.add(name); + } + } + + return Collections.unmodifiableList(result); } /* @@ -90,4 +116,13 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping { public Method getMethod() { return method; } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.MethodResourceMapping#getParameterNames() + */ + @Override + public List getParameterNames() { + return parameterNames; + } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java index 2e1d25f3a..a8baf895c 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 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. @@ -15,20 +15,20 @@ */ package org.springframework.data.rest.core.mapping; -import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.lang.reflect.Method; import org.junit.Test; import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.annotation.RestResource; -import org.springframework.data.rest.core.mapping.RepositoryCollectionResourceMapping; -import org.springframework.data.rest.core.mapping.RepositoryMethodResourceMapping; -import org.springframework.data.rest.core.mapping.ResourceMapping; /** + * Unit tests for {@link RepositoryMethodResourceMapping}. + * * @author Oliver Gierke */ public class RepositoryMethodResourceMappingUnitTests { @@ -36,7 +36,7 @@ public class RepositoryMethodResourceMappingUnitTests { RepositoryCollectionResourceMapping resourceMapping = new RepositoryCollectionResourceMapping(PersonRepository.class); @Test - public void foo() throws Exception { + public void defaultsMappingToMethodName() throws Exception { Method method = PersonRepository.class.getMethod("findByLastname", String.class); ResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); @@ -53,6 +53,31 @@ public class RepositoryMethodResourceMappingUnitTests { assertThat(mapping.getPath(), is(new Path("bar"))); } + /** + * @see DATAREST-31 + */ + @Test + public void doesNotDiscoverAnyParametersIfNotAnnotated() throws Exception { + + Method method = PersonRepository.class.getMethod("findByLastname", String.class); + MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + + assertThat(mapping.getParameterNames(), is(emptyIterable())); + } + + /** + * @see DATAREST-31 + */ + @Test + public void resolvesParameterNamesIfNotAnnotated() throws Exception { + + Method method = PersonRepository.class.getMethod("findByFirstname", String.class); + MethodResourceMapping mapping = new RepositoryMethodResourceMapping(method, resourceMapping); + + assertThat(mapping.getParameterNames(), hasSize(1)); + assertThat(mapping.getParameterNames(), hasItem("firstname")); + } + static class Person {} interface PersonRepository extends Repository { @@ -60,7 +85,7 @@ public class RepositoryMethodResourceMappingUnitTests { Iterable findByLastname(String lastname); @RestResource(path = "/bar") - Iterable findByFirstname(String firstname); + Iterable findByFirstname(@Param("firstname") String firstname); @RestResource(path = "foo") Iterable findByEmailAddress(String email); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index f064613ba..a7284dac4 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 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. @@ -19,13 +19,14 @@ import static org.springframework.data.rest.webmvc.ControllerUtils.*; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; import org.springframework.data.rest.core.invoke.RepositoryInvoker; -import org.springframework.data.rest.core.mapping.ResourceMapping; +import org.springframework.data.rest.core.mapping.MethodResourceMapping; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.SearchResourceMappings; @@ -33,6 +34,7 @@ import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkBuilder; +import org.springframework.hateoas.LinkTemplate; import org.springframework.hateoas.Links; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; @@ -40,6 +42,7 @@ import org.springframework.hateoas.Resources; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -56,6 +59,7 @@ class RepositorySearchController extends AbstractRepositoryRestController { private static final String SEARCH = "/search"; private static final String BASE_MAPPING = "/{repository}" + SEARCH; + private static final String PARAMETER_NAME_TEMPALTE_PATTERN = "{?%s}"; private final EntityLinks entityLinks; private final ResourceMappings mappings; @@ -223,15 +227,23 @@ class RepositorySearchController extends AbstractRepositoryRestController { SearchResourceMappings searchMappings = mappings.getSearchResourceMappings(domainType); LinkBuilder builder = entityLinks.linkFor(domainType).slash(searchMappings.getPath()); - for (ResourceMapping mapping : searchMappings) { + for (MethodResourceMapping mapping : searchMappings) { if (!mapping.isExported()) { continue; } - links.add(builder.slash(mapping.getPath()).withRel(mapping.getRel())); + String parameterTemplateVariable = getParameterTemplateVariable(mapping.getParameterNames()); + String href = builder.slash(mapping.getPath()).toString().concat(parameterTemplateVariable); + + links.add(new LinkTemplate(href, mapping.getRel())); } return new Links(links); } + + private static String getParameterTemplateVariable(Collection parameters) { + String parameterString = StringUtils.collectionToCommaDelimitedString(parameters); + return parameters.isEmpty() ? "" : String.format(PARAMETER_NAME_TEMPALTE_PATTERN, parameterString); + } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java index c5a51e666..3f98210b4 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositorySearchControllerIntegrationTests.java @@ -60,11 +60,10 @@ public class RepositorySearchControllerIntegrationTests extends AbstractControll ResourceTester tester = ResourceTester.of(resource); tester.assertNumberOfLinks(4); - tester.assertHasLink("findFirstPersonByFirstName", "http://localhost/people/search/findFirstPersonByFirstName"); - tester.assertHasLink("firstname", "http://localhost/people/search/firstname"); - tester.assertHasLink("findByCreatedUsingISO8601Date", - "http://localhost/people/search/findByCreatedUsingISO8601Date"); - tester.assertHasLink("findByCreatedGreaterThan", "http://localhost/people/search/findByCreatedGreaterThan"); + tester.assertHasLinkEndingWith("findFirstPersonByFirstName", "findFirstPersonByFirstName{?firstName}"); + tester.assertHasLinkEndingWith("firstname", "firstname{?firstName}"); + tester.assertHasLinkEndingWith("findByCreatedUsingISO8601Date", "findByCreatedUsingISO8601Date{?date}"); + tester.assertHasLinkEndingWith("findByCreatedGreaterThan", "findByCreatedGreaterThan{?date}"); } @Test(expected = ResourceNotFoundException.class) diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceTester.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceTester.java index 0fc7ed6d9..8d0901936 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceTester.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceTester.java @@ -18,6 +18,7 @@ package org.springframework.data.rest.webmvc; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import org.hamcrest.Matcher; import org.springframework.data.rest.core.Path; import org.springframework.hateoas.Link; import org.springframework.hateoas.PagedResources; @@ -61,19 +62,33 @@ public class ResourceTester { } /** - * Asserts that the {@link Resource} has a linke with the given rel and href. + * Asserts that the {@link Resource} has a link with the given rel and href. * * @param rel must not be {@literal null}. * @param href can be {@literal null}, if so, only the presence of a {@link Link} with the given rel is checked. */ public Link assertHasLink(String rel, String href) { + return assertHasLinkMatching(rel, href == null ? null : is(href)); + } + + /** + * Asserts that the {@link Resource} has a link with the given rel and ending with the given href. + * + * @param rel must not be {@literal null}. + * @param href can be {@literal null}, if so, only the presence of a {@link Link} with the given rel is checked. + */ + public Link assertHasLinkEndingWith(String rel, String hrefEnd) { + return assertHasLinkMatching(rel, hrefEnd == null ? null : endsWith(hrefEnd)); + } + + private final Link assertHasLinkMatching(String rel, Matcher hrefMatcher) { Link link = resource.getLink(rel); assertThat("Expected link with rel '" + rel + "' but didn't find it in " + resource.getLinks(), link, is(notNullValue())); - if (href != null) { - assertThat(link.getHref(), is(href)); + if (hrefMatcher != null) { + assertThat(link.getHref(), is(hrefMatcher)); } return link;