From a4d8a22428867f6b6c9b8239e2e6cb983448f472 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 12 Jul 2013 14:20:32 +0200 Subject: [PATCH] DATAREST-93 - Further refactorings and refinements regarding the search resource mapping. --- .../springframework/data/rest/core/Path.java | 5 + .../data/rest/core/PathUnitTests.java | 18 +++- .../mapping/MethodResourceMapping.java | 33 ++++++ .../RepositoryAwareResourceInformation.java | 20 ++-- .../RepositoryMethodResourceMapping.java | 18 +++- .../repository/mapping/ResourceMappings.java | 21 ++-- .../repository/mapping/ResourceMetadata.java | 45 ++++++-- .../mapping/SearchResourceMappings.java | 101 ++++++++++++++++++ .../AbstractRepositoryRestController.java | 7 +- .../webmvc/RepositoryEntityController.java | 28 +++-- ...RepositoryPropertyReferenceController.java | 25 ++--- .../rest/webmvc/RepositoryRestRequest.java | 18 +++- ...tRequestHandlerMethodArgumentResolver.java | 2 + .../webmvc/RepositorySearchController.java | 78 ++++++-------- ...MetadataHandlerMethodArgumentResolver.java | 2 +- .../RepositoryRestMvcConfiguration.java | 25 ++--- .../json/PersistentEntityJackson2Module.java | 2 +- ...PersistentEntityToJsonSchemaConverter.java | 2 +- .../webmvc/support/RepositoryLinkBuilder.java | 2 +- .../webmvc/json/RepositoryTestsConfig.java | 2 +- 20 files changed, 332 insertions(+), 122 deletions(-) create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/MethodResourceMapping.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/SearchResourceMappings.java diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Path.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Path.java index 7acbe0fb7..e88ff8bdb 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Path.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Path.java @@ -26,6 +26,7 @@ import org.springframework.util.StringUtils; public class Path { private static final String SLASH = "/"; + private static final String MATCH_PATTERN = "/?%s"; private final String path; @@ -48,6 +49,10 @@ public class Path { this.path = cleanUp ? cleanUp(path) : path; } + public boolean matches(String reference) { + return this.path.matches(String.format(MATCH_PATTERN, reference)); + } + /** * Appends the given {@link String} to the current {@link Path}. * diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/PathUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/PathUnitTests.java index 572a80143..10b4c9767 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/PathUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/PathUnitTests.java @@ -19,9 +19,10 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import org.junit.Test; -import org.springframework.data.rest.core.Path; /** + * Unit tests for {@link Path}. + * * @author Oliver Gierke */ public class PathUnitTests { @@ -46,4 +47,19 @@ public class PathUnitTests { Path builder = new Path("foo/ ").slash("/ b a r").slash(" //foobar/// "); assertThat(builder.toString(), is("/foo/bar/foobar")); } + + @Test + public void matchesWithLeadingSlash() { + assertThat(new Path("/foobar").matches("/foobar"), is(true)); + } + + @Test + public void matchesWithoutLeadingSlash() { + assertThat(new Path("/foobar").matches("foobar"), is(true)); + } + + @Test + public void doesNotMatchIfDifferent() { + assertThat(new Path("/foobar").matches("barfoo"), is(false)); + } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/MethodResourceMapping.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/MethodResourceMapping.java new file mode 100644 index 000000000..d7aff15f1 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/MethodResourceMapping.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013 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.data.rest.repository.mapping; + +import java.lang.reflect.Method; + +/** + * A {@link ResourceMapping} that is backed by a {@link Method}. + * + * @author Oliver Gierke + */ +public interface MethodResourceMapping extends ResourceMapping { + + /** + * Returns the {@link Method} backing the resource. + * + * @return + */ + Method getMethod(); +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceInformation.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceInformation.java index b0807b80a..70fd54854 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceInformation.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceInformation.java @@ -15,8 +15,6 @@ */ package org.springframework.data.rest.repository.mapping; -import java.util.Map; - import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.mapping.PersistentProperty; @@ -57,12 +55,23 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { return AnnotationUtils.findAnnotation(repositoryInterface.getRepositoryInterface(), Primary.class) != null; } + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMetadata#getDomainType() + */ + @Override + public Class getDomainType() { + return repositoryInterface.getDomainType(); + } + /* * (non-Javadoc) * @see org.springframework.data.rest.repository.mapping.DelegatingResourceInformation#isManaged(org.springframework.data.mapping.PersistentProperty) */ @Override - public boolean isManaged(PersistentProperty property) { + public boolean isManagedResource(PersistentProperty property) { + + Assert.notNull(property, "PersistentProperty must not be null!"); return repositories.hasRepositoryFor(property.getActualType()); } @@ -80,7 +89,7 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { * @see org.springframework.data.rest.repository.mapping.ResourceMetadataProvider#hasMappingFor(org.springframework.data.mapping.PersistentProperty) */ @Override - public boolean isMapped(PersistentProperty property) { + public boolean isExported(PersistentProperty property) { return provider.isMapped(property); } @@ -125,8 +134,7 @@ class RepositoryAwareResourceInformation implements ResourceMetadata { * @see org.springframework.data.rest.repository.mapping.ResourceMetadata#getSearchResourceMappings() */ @Override - public Map getSearchResourceMappings() { + public SearchResourceMappings getSearchResourceMappings() { return provider.getSearchResourceMappings(repositoryInterface.getRepositoryInterface()); } - } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryMethodResourceMapping.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryMethodResourceMapping.java index 4c6a6bbf5..51428d051 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryMethodResourceMapping.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryMethodResourceMapping.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.repository.annotation.RestResource; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -27,19 +28,24 @@ import org.springframework.util.StringUtils; * * @author Oliver Gierke */ -class RepositoryMethodResourceMapping implements ResourceMapping { +class RepositoryMethodResourceMapping implements MethodResourceMapping { private final boolean isExported; private final String rel; private final Path path; + private final Method method; /** * Creates a new {@link RepositoryMethodResourceMapping} for the given {@link Method}. * * @param method must not be {@literal null}. + * @param resourceMapping must not be {@literal null}. */ public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMapping) { + Assert.notNull(method, "Method must not be null!"); + Assert.notNull(resourceMapping, "ResourceMapping must not be null!"); + RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class); this.isExported = annotation != null ? annotation.exported() : true; @@ -49,6 +55,7 @@ class RepositoryMethodResourceMapping implements ResourceMapping { String toAppend = annotation == null || !StringUtils.hasText(annotation.path()) ? method.getName() : annotation .path(); this.path = resourcePath.slash(toAppend); + this.method = method; } /* @@ -77,4 +84,13 @@ class RepositoryMethodResourceMapping implements ResourceMapping { public Path getPath() { return path; } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.MethodResourceMapping#getMethod() + */ + @Override + public Method getMethod() { + return method; + } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappings.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappings.java index e7ceccafc..d3db6bffe 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappings.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappings.java @@ -16,8 +16,10 @@ package org.springframework.data.rest.repository.mapping; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import org.springframework.data.mapping.PersistentProperty; @@ -37,12 +39,11 @@ import org.springframework.util.Assert; */ public class ResourceMappings implements Iterable { - private final Repositories repositories; private final RelProvider relProvider; private final Map, ResourceMetadata> cache = new HashMap, ResourceMetadata>(); - private final Map, Map> searchCache = new HashMap, Map>(); + private final Map, SearchResourceMappings> searchCache = new HashMap, SearchResourceMappings>(); /** * Creates a new {@link ResourceMappings} using the given {@link RepositoryRestConfiguration} and {@link Repositories} @@ -109,10 +110,12 @@ public class ResourceMappings implements Iterable { /** * Returns the {@link ResourceMapping}s for the search resources of the given type. * - * @param type + * @param type must not be {@literal null}. * @return */ - public Map getSearchResourceMappings(Class type) { + public SearchResourceMappings getSearchResourceMappings(Class type) { + + Assert.notNull(type, "Type must not be null!"); if (searchCache.containsKey(type)) { return searchCache.get(type); @@ -120,16 +123,16 @@ public class ResourceMappings implements Iterable { Class domainType = RepositoriesUtils.getDomainType(type); RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(domainType); - Map mappings = new HashMap(); + List mappings = new ArrayList(); ResourceMetadata repositoryMapping = getMappingFor(repositoryInformation.getRepositoryInterface()); for (Method queryMethod : repositoryInformation.getQueryMethods()) { - mappings.put(queryMethod.getName(), new RepositoryMethodResourceMapping(queryMethod, - repositoryMapping)); + mappings.add(new RepositoryMethodResourceMapping(queryMethod, repositoryMapping)); } - searchCache.put(type, mappings); - return mappings; + SearchResourceMappings searchMappings = new SearchResourceMappings(mappings); + searchCache.put(type, searchMappings); + return searchMappings; } /** diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadata.java index 4db9a95a6..67137abc7 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadata.java @@ -15,20 +15,51 @@ */ package org.springframework.data.rest.repository.mapping; -import java.util.Map; - import org.springframework.data.mapping.PersistentProperty; /** + * Interface for metadata of resources exposed throught the system. + * * @author Oliver Gierke */ public interface ResourceMetadata extends CollectionResourceMapping { - boolean isManaged(PersistentProperty property); - - boolean isMapped(PersistentProperty property); + /** + * Returns the domain type that is exposed through the resource. + * + * @return + */ + Class getDomainType(); + /** + * Returns whether the type of the given {@link PersistentProperty} is exposed as resource itself. + * + * @param property must not be {@literal null}. + * @return + */ + boolean isManagedResource(PersistentProperty property); + + /** + * Returns whether the given {@link PersistentProperty} is a managed resource and in fact exported. + * + * @param property must not be {@literal null}. + * @return + */ + boolean isExported(PersistentProperty property); + + /** + * Returns the {@link ResourceMapping} for the given {@link PersistentProperty} or {@literal null} if not managed. + * + * @param property must not be {@literal null}. + * @return + */ ResourceMapping getMappingFor(PersistentProperty property); - - Map getSearchResourceMappings(); + + /** + * Returns the {@link SearchResourceMappings}, i.e. the mappings for the search resource exposed for the current + * resource. + * + * @return + */ + SearchResourceMappings getSearchResourceMappings(); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/SearchResourceMappings.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/SearchResourceMappings.java new file mode 100644 index 000000000..8b829dbf5 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/SearchResourceMappings.java @@ -0,0 +1,101 @@ +/* + * Copyright 2013 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.data.rest.repository.mapping; + +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.List; + +import org.springframework.data.rest.core.Path; +import org.springframework.util.Assert; + +/** + * {@link ResourceMapping} for all search resources. + * + * @author Oliver Gierke + */ +public class SearchResourceMappings implements Iterable, ResourceMapping { + + private static final Path PATH = new Path("/search"); + private static final String REL = "search"; + + private final List mappings; + + /** + * Creates a new {@link SearchResourceMappings} from the given + * + * @param mappings + */ + public SearchResourceMappings(List mappings) { + + Assert.notNull(mappings, "MethodResourceMappings must not be null!"); + this.mappings = mappings; + } + + /** + * Returns the method mapped to the given path. + * + * @param path must not be {@literal null} or empty. + * @return + */ + public Method getMappedMethod(String path) { + + Assert.hasText(path, "Path must not be null or empty!"); + + for (MethodResourceMapping mapping : mappings) { + if (mapping.getPath().matches(path)) { + return mapping.getMethod(); + } + } + + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMapping#getPath() + */ + @Override + public Path getPath() { + return PATH; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMapping#getRel() + */ + @Override + public String getRel() { + return REL; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMapping#isExported() + */ + @Override + public Boolean isExported() { + return !mappings.isEmpty(); + } + + /* (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return mappings.iterator(); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java index b07408791..32fbd5c0d 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java @@ -56,7 +56,7 @@ import org.springframework.web.bind.annotation.ResponseBody; * @author Jon Brisbin * @author Oliver Gierke */ -@SuppressWarnings({ "rawtypes", "deprecation" }) +@SuppressWarnings({ "rawtypes" }) class AbstractRepositoryRestController implements MessageSourceAware, InitializingBean { private static final Logger LOG = LoggerFactory.getLogger(AbstractRepositoryRestController.class); @@ -196,11 +196,12 @@ class AbstractRepositoryRestController implements MessageSourceAware, Initializi } protected Link resourceLink(RepositoryRestRequest repoRequest, Resource resource) { + ResourceMetadata repoMapping = repoRequest.getRepositoryResourceMapping(); - ResourceMetadata entityMapping = repoRequest.getPersistentEntityResourceMapping(); Link selfLink = resource.getLink("self"); - String rel = repoMapping.getRel() + "." + entityMapping.getRel(); + String rel = repoMapping.getSingleResourceRel(); + return new Link(selfLink.getHref(), rel); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index d8131c6d8..5d22072bf 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -35,7 +35,6 @@ import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.config.RepositoryRestConfiguration; -import org.springframework.data.rest.config.ResourceMapping; import org.springframework.data.rest.repository.PersistentEntityResource; import org.springframework.data.rest.repository.context.AfterCreateEvent; import org.springframework.data.rest.repository.context.AfterDeleteEvent; @@ -45,6 +44,7 @@ import org.springframework.data.rest.repository.context.BeforeDeleteEvent; import org.springframework.data.rest.repository.context.BeforeSaveEvent; import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; import org.springframework.data.rest.repository.mapping.ResourceMetadata; +import org.springframework.data.rest.repository.mapping.SearchResourceMappings; import org.springframework.data.rest.repository.support.DomainObjectMerger; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.EntityLinks; @@ -69,7 +69,6 @@ import org.springframework.web.bind.annotation.ResponseBody; * @author Oliver Gierke */ @RestController -@SuppressWarnings("deprecation") class RepositoryEntityController extends AbstractRepositoryRestController implements ApplicationEventPublisherAware { private static final String BASE_MAPPING = "/{repository}"; @@ -112,15 +111,16 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem this.publisher = publisher; } + @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/json", "application/x-spring-data-verbose+json" }) - @ResponseBody public Resources listEntities(final RepositoryRestRequest request, Pageable pageable) throws ResourceNotFoundException { - List links = new ArrayList(); + List links = new ArrayList(); Iterable results; RepositoryMethodInvoker repoMethodInvoker = request.getRepositoryMethodInvoker(); + if (null == repoMethodInvoker) { throw new ResourceNotFoundException(); } @@ -136,9 +136,12 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem } ResourceMetadata repoMapping = request.getRepositoryResourceMapping(); - if (!repoMethodInvoker.getQueryMethods().isEmpty()) { - links.add(entityLinks.linkForSingleResource(request.getPersistentEntity().getType(), "search").withRel( - repoMapping.getRel() + ".search")); + + SearchResourceMappings searchMappings = repoMapping.getSearchResourceMappings(); + + if (searchMappings.isExported()) { + links.add(entityLinks.linkFor(repoMapping.getDomainType()).slash(searchMappings.getPath()) + .withRel(searchMappings.getRel())); } Resources resources = resultToResources(results); @@ -270,10 +273,13 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem && !(repoMethodInvoker.hasDeleteOne() || repoMethodInvoker.hasDeleteOneById())) { throw new HttpRequestMethodNotSupportedException("DELETE"); } - ResourceMapping methodMapping = repoRequest.getRepositoryResourceMapping().getResourceMappingFor("delete"); - if (null != methodMapping && !methodMapping.isExported()) { - throw new HttpRequestMethodNotSupportedException("DELETE"); - } + + // TODO: re-enable not exposing delete method if hidden + + // ResourceMapping methodMapping = repoRequest.getRepositoryResourceMapping().getResourceMappingFor("delete"); + // if (null != methodMapping && !methodMapping.isExported()) { + // throw new HttpRequestMethodNotSupportedException("DELETE"); + // } final Object domainObj = converter.convert(id, STRING_TYPE, TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType())); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index 0eac5aaca..3b296521f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -44,6 +44,7 @@ import org.springframework.data.rest.repository.context.AfterLinkSaveEvent; import org.springframework.data.rest.repository.context.BeforeLinkDeleteEvent; import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent; import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.repository.mapping.ResourceMetadata; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; @@ -115,7 +116,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes List> resources = new ArrayList>(); - for (Object obj : ((Iterable) prop.propertyValue)) { + for (Object obj : (Iterable) prop.propertyValue) { resources.add(perAssembler.toResource(obj)); } @@ -198,7 +199,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes throw new ResourceNotFoundException(); } if (prop.property.isCollectionLike()) { - for (Object obj : ((Iterable) prop.propertyValue)) { + for (Object obj : (Iterable) prop.propertyValue) { BeanWrapper propValWrapper = BeanWrapper.create(obj, null); String sId = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); @@ -244,22 +245,19 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes return response; } - ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping(); - ResourceMapping entityMapping = repoRequest.getPersistentEntityResourceMapping(); - String propName = entityMapping.getNameForPath(property); - ResourceMapping propMapping = entityMapping.getResourceMappingFor(entityMapping.getNameForPath(property)); - PersistentProperty persistentProp = repoRequest.getPersistentEntity().getPersistentProperty(propName); - Class propType = (persistentProp.isCollectionLike() || persistentProp.isMap() ? persistentProp - .getComponentType() : persistentProp.getType()); + ResourceMetadata repoMapping = repoRequest.getRepositoryResourceMapping(); + PersistentProperty persistentProp = repoRequest.getPersistentEntity().getPersistentProperty(property); + + Class propType = persistentProp.isCollectionLike() || persistentProp.isMap() ? persistentProp.getComponentType() + : persistentProp.getType(); ResourceMapping propRepoMapping = getResourceMapping(config, repositories.getRepositoryInformationFor(propType)); - String propRel = String.format("%s.%s.%s.%s", repoMapping.getRel(), entityMapping.getRel(), - (null != propMapping ? propMapping.getRel() : property), propRepoMapping.getRel()); + String propRel = String.format("%s.%s.%s", repoMapping.getSingleResourceRel(), property, propRepoMapping.getRel()); Resource resource = response.getBody(); List links = new ArrayList(); - URI entityBaseUri = buildUri(repoRequest.getBaseUri(), repoMapping.getPath(), id, property); + URI entityBaseUri = buildUri(repoRequest.getBaseUri(), repoMapping.getPath().toString(), id, property); if (resource.getContent() instanceof Iterable) { for (Resource res : (Iterable>) resource.getContent()) { @@ -410,8 +408,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes throw new ResourceNotFoundException(); } - String propertyName = repoRequest.getPersistentEntityResourceMapping().getNameForPath(propertyPath); - PersistentProperty prop = repoRequest.getPersistentEntity().getPersistentProperty(propertyName); + PersistentProperty prop = repoRequest.getPersistentEntity().getPersistentProperty(propertyPath); if (null == prop) { throw new ResourceNotFoundException(); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java index 37504583c..1311d7581 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java @@ -15,43 +15,51 @@ */ package org.springframework.data.rest.webmvc; +import java.io.Serializable; import java.net.URI; import javax.servlet.http.HttpServletRequest; import org.springframework.core.convert.ConversionService; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.config.RepositoryRestConfiguration; import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; import org.springframework.data.rest.repository.mapping.ResourceMetadata; -import org.springframework.hateoas.Link; /** * @author Jon Brisbin * @author Oliver Gierke */ -@SuppressWarnings("deprecation") class RepositoryRestRequest { private final HttpServletRequest request; private final URI baseUri; private final ResourceMetadata resourceMetadata; - private final Link repoLink; private final RepositoryMethodInvoker repoMethodInvoker; private final PersistentEntity persistentEntity; public RepositoryRestRequest(RepositoryRestConfiguration config, Repositories repositories, HttpServletRequest request, URI baseUri, ResourceMetadata repoInfo, ConversionService conversionService) { + this.request = request; this.baseUri = baseUri; this.resourceMetadata = repoInfo; if (resourceMetadata == null || !resourceMetadata.isExported()) { - this.repoLink = null; + this.repoMethodInvoker = null; this.persistentEntity = null; + } else { - this.persistentEntity = repositories.getPersistentEntity(repoInfo.getDomainType()); + + Class domainType = repoInfo.getDomainType(); + CrudRepository repositoryFor = repositories.getRepositoryFor(domainType); + RepositoryInformation information = repositories.getRepositoryInformationFor(domainType); + + this.repoMethodInvoker = new RepositoryMethodInvoker(repositoryFor, information, conversionService); + this.persistentEntity = repositories.getPersistentEntity(domainType); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java index fa763aa00..9a06081b2 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java @@ -67,6 +67,8 @@ public class RepositoryRestRequestHandlerMethodArgumentResolver implements Handl URI baseUri = baseUriResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); ResourceMetadata repoInfo = repoInfoResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + // TODO reject if ResourceMetadata cannot be resolved + return new RepositoryRestRequest(config, repositories, webRequest.getNativeRequest(HttpServletRequest.class), baseUri, repoInfo, conversionService); } 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 2f1684966..b119f00a8 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 @@ -15,10 +15,7 @@ */ package org.springframework.data.rest.webmvc; -import static org.springframework.data.rest.core.util.UriUtils.*; -import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; import static org.springframework.data.rest.webmvc.ControllerUtils.*; -import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; import java.lang.reflect.Method; import java.net.URI; @@ -29,15 +26,14 @@ import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.config.RepositoryRestConfiguration; -import org.springframework.data.rest.config.ResourceMapping; import org.springframework.data.rest.repository.invoke.RepositoryMethod; import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; -import org.springframework.data.rest.repository.support.ResourceMappingUtils; -import org.springframework.data.rest.webmvc.support.BaseUriLinkBuilder; +import org.springframework.data.rest.repository.mapping.ResourceMapping; +import org.springframework.data.rest.repository.mapping.ResourceMappings; +import org.springframework.data.rest.repository.mapping.ResourceMetadata; +import org.springframework.data.rest.repository.mapping.SearchResourceMappings; 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.Resource; @@ -53,22 +49,21 @@ import org.springframework.web.bind.annotation.ResponseBody; * @author Oliver Gierke */ @RestController -@SuppressWarnings("deprecation") class RepositorySearchController extends AbstractRepositoryRestController { private static final String BASE_MAPPING = "/{repository}/search"; - private final Repositories repositories; - private final RepositoryRestConfiguration config; + private final EntityLinks entityLinks; + private final ResourceMappings mappings; @Autowired - public RepositorySearchController(Repositories repositories, RepositoryRestConfiguration config, - PagedResourcesAssembler assembler, PersistentEntityResourceAssembler perAssembler) { + public RepositorySearchController(PagedResourcesAssembler assembler, + PersistentEntityResourceAssembler perAssembler, EntityLinks entityLinks, ResourceMappings mappings) { super(assembler, perAssembler); - this.repositories = repositories; - this.config = config; + this.entityLinks = entityLinks; + this.mappings = mappings; } @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/json", @@ -84,20 +79,19 @@ class RepositorySearchController extends AbstractRepositoryRestController { } protected List queryMethodLinks(URI baseUri, Class domainType) { + List links = new ArrayList(); - RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(domainType); - ResourceMapping repoMapping = ResourceMappingUtils.merge(repoInfo.getRepositoryInterface(), - config.getResourceMappingForRepository(repoInfo.getRepositoryInterface())); - for (Method method : repoInfo.getQueryMethods()) { - LinkBuilder linkBuilder = BaseUriLinkBuilder.create(buildUri(baseUri, repoMapping.getPath(), "search")); - ResourceMapping methodMapping = ResourceMappingUtils.merge(method, - repoMapping.getResourceMappingFor(method.getName())); - if (!methodMapping.isExported()) { + LinkBuilder builder = entityLinks.linkFor(domainType).slash("search"); + + for (ResourceMapping mapping : mappings.getSearchResourceMappings(domainType)) { + + if (!mapping.isExported()) { continue; } - links - .add(linkBuilder.slash(methodMapping.getPath()).withRel(repoMapping.getRel() + "." + methodMapping.getRel())); + + links.add(builder.slash(mapping.getPath().toString()).withRel(mapping.getRel())); } + return links; } @@ -106,34 +100,28 @@ class RepositorySearchController extends AbstractRepositoryRestController { @ResponseBody public ResourceSupport query(final RepositoryRestRequest repoRequest, @PathVariable String repository, @PathVariable String method, Pageable pageable) throws ResourceNotFoundException { + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + if (repoMethodInvoker.getQueryMethods().isEmpty()) { throw new ResourceNotFoundException(); } - ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping(); - String methodName = repoMapping.getNameForPath(method); - RepositoryMethod repoMethod = repoMethodInvoker.getQueryMethods().get(methodName); - if (null == repoMethod) { - for (RepositoryMethod queryMethod : repoMethodInvoker.getQueryMethods().values()) { - String path = findPath(queryMethod.getMethod()); - if (path.equals(method)) { - repoMethod = queryMethod; - break; - } - } - if (null == repoMethod) { - throw new ResourceNotFoundException(); - } + ResourceMetadata repoMapping = repoRequest.getRepositoryResourceMapping(); + + SearchResourceMappings searchResourceMappings = repoMapping.getSearchResourceMappings(); + Method mappedMethod = searchResourceMappings.getMappedMethod(method); + + if (mappedMethod == null) { + throw new ResourceNotFoundException(); } + RepositoryMethod repositoryMethod = new RepositoryMethod(mappedMethod); + Map rawParameters = repoRequest.getRequest().getParameterMap(); - Object result = repoMethodInvoker.invokeQueryMethod(repoMethod, pageable, rawParameters); + Object result = repoMethodInvoker.invokeQueryMethod(repositoryMethod, pageable, rawParameters); - Link baseLink = linkTo(methodOn(RepositorySearchController.class). // - queryCompact(repoRequest, repository, methodName, pageable)).withSelfRel(); - - return resultToResources(result, baseLink); + return resultToResources(result); } @RequestMapping(value = BASE_MAPPING + "/{method}", method = RequestMethod.GET, diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceMetadataHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceMetadataHandlerMethodArgumentResolver.java index 64acd14e0..c9bec0947 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceMetadataHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceMetadataHandlerMethodArgumentResolver.java @@ -96,7 +96,7 @@ public class ResourceMetadataHandlerMethodArgumentResolver implements HandlerMet for (Class domainType : repositories) { ResourceMetadata mapping = mappings.getMappingFor(domainType); - if (pathSegment.equals(mapping.getPath()) && mapping.isExported()) { + if (mapping.getPath().matches(pathSegment) && mapping.isExported()) { return mapping; } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index b2db542fe..420f36bd9 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -43,10 +43,10 @@ import org.springframework.data.rest.repository.support.DomainObjectMerger; import org.springframework.data.rest.webmvc.BaseUriMethodArgumentResolver; import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler; import org.springframework.data.rest.webmvc.PersistentEntityResourceHandlerMethodArgumentResolver; -import org.springframework.data.rest.webmvc.RepositoryInformationHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter; import org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping; import org.springframework.data.rest.webmvc.RepositoryRestRequestHandlerMethodArgumentResolver; +import org.springframework.data.rest.webmvc.ResourceMetadataHandlerMethodArgumentResolver; import org.springframework.data.rest.webmvc.RestController; import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver; import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter; @@ -119,7 +119,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon @Bean public UriDomainClassConverter uriDomainClassConverter() { - return new UriDomainClassConverter(); + return new UriDomainClassConverter(repositories(), domainClassConverter()); } /** @@ -195,16 +195,6 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new BaseUriMethodArgumentResolver(config()); } - /** - * Resolves the {@link org.springframework.data.repository.core.RepositoryInformation} for this request. - * - * @return - */ - @Bean - public RepositoryInformationHandlerMethodArgumentResolver repoInfoMethodArgumentResolver() { - return new RepositoryInformationHandlerMethodArgumentResolver(); - } - /** * Turns an {@link javax.servlet.http.HttpServletRequest} into a * {@link org.springframework.http.server.ServerHttpRequest}. @@ -226,6 +216,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return new RepositoryRestRequestHandlerMethodArgumentResolver(defaultConversionService()); } + @Bean + public ResourceMetadataHandlerMethodArgumentResolver resourceMetadataHandlerMethodArgumentResolver() { + return new ResourceMetadataHandlerMethodArgumentResolver(repositories(), resourceMappings()); + } + /** * A special {@link org.springframework.hateoas.EntityLinks} implementation that takes repository and current * configuration into account when generating links. @@ -258,7 +253,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() { - return new PersistentEntityToJsonSchemaConverter(); + return new PersistentEntityToJsonSchemaConverter(repositories(), resourceMappings()); } /** @@ -391,8 +386,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon private List defaultMethodArgumentResolvers() { return Arrays.asList(baseUriMethodArgumentResolver(), pageableResolver(), sortResolver(), - serverHttpRequestMethodArgumentResolver(), repoInfoMethodArgumentResolver(), repoRequestArgumentResolver(), - persistentEntityArgumentResolver()); + serverHttpRequestMethodArgumentResolver(), repoRequestArgumentResolver(), persistentEntityArgumentResolver(), + resourceMetadataHandlerMethodArgumentResolver()); } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index cc53ad348..3252cf800 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -74,7 +74,7 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init Assert.isTrue(persistentProperty.isAssociation(), "PersistentProperty must be an association!"); ResourceMetadata metadata = mappings.getMappingFor(persistentProperty.getOwner().getType()); - if (!metadata.isManaged(persistentProperty)) { + if (!metadata.isManagedResource(persistentProperty)) { return false; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java index 7997555b1..c9b9ad621 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java @@ -121,7 +121,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric PersistentProperty persistentProperty = association.getInverse(); - if (!metadata.isMapped(persistentProperty)) { + if (!metadata.isExported(persistentProperty)) { return; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java index 7c9627c94..b792d1cab 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java @@ -60,7 +60,7 @@ public class RepositoryLinkBuilder extends LinkBuilderSupport