From db2bbfca0e93f144004d93df7d8520818d7cdddf Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 14 Jun 2013 19:53:54 +0200 Subject: [PATCH] DATAREST-93 - Quite a lot of refactoring. Integrated pagination resolving and closer integration of Spring Data Commons configuration. --- .gitignore | 1 + build.gradle | 6 +- .../config/RepositoryRestConfiguration.java | 18 +- .../data/rest/config/ResourceMapping.java | 16 + .../config/ResourceMappingConfiguration.java | 16 + .../rest/repository/BaseUriAwareResource.java | 68 --- .../repository/BaseUriAwareResources.java | 73 ---- .../rest/repository/PagingAndSorting.java | 85 ---- .../repository/PersistentEntityResource.java | 67 +-- .../context/RepositoriesFactoryBean.java | 33 -- .../invoke/RepositoryMethodInvoker.java | 58 ++- .../mapping/CollectionResourceMapping.java | 26 ++ .../CollectionResourceMappingBuilder.java | 115 +++++ .../mapping/InternalMappingBuilder.java | 26 ++ .../repository/mapping/MappingBuilder.java | 33 ++ .../RepositoryAwareResourceInformation.java | 119 ++++++ .../RepositoryMethodResourceMapping.java | 74 ++++ .../repository/mapping/ResourceMapping.java | 63 +++ .../mapping/ResourceMappingFactory.java | 144 +++++++ .../repository/mapping/ResourceMappings.java | 215 ++++++++++ .../repository/mapping/ResourceMetadata.java | 27 ++ .../mapping/ResourceMetadataProvider.java | 29 ++ .../SimpleCollectionResourceMapping.java | 69 +++ .../repository/support/RepositoriesUtils.java | 49 +++ .../support/RepositoryInformationSupport.java | 3 +- .../support/RepositoryRelProvider.java | 64 +++ .../support/ResourceMappingUtils.java | 1 + .../repository/support/SimpleRelProvider.java | 53 +++ .../rest/config/ResourceMappingUnitTests.java | 56 +-- ...toryRestConfigurationIntegrationTests.java | 3 +- .../repository/RepositoryTestsConfig.java | 17 +- .../jpa/ConfiguredPersonRepository.java | 2 + .../domain/jpa/JpaRepositoryConfig.java | 18 +- .../repository/domain/jpa/PersonLoader.java | 2 +- .../domain/jpa/PlainPersonRepository.java | 2 + ...yAwareResourceMappingFactoryUnitTests.java | 60 +++ .../ResourceMappingFactoryUnitTests.java | 90 ++++ .../ResourceMappingsIntegrationTest.java | 60 +++ .../src/test/resources/logback.xml | 16 + .../AbstractRepositoryRestController.java | 370 +++++----------- .../webmvc/BaseUriMethodArgumentResolver.java | 26 +- .../data/rest/webmvc/ControllerUtils.java | 49 +++ ...agingAndSortingMethodArgumentResolver.java | 144 ------- .../PersistentEntityResourceAssembler.java | 74 ++++ ...ResourceHandlerMethodArgumentResolver.java | 3 +- .../rest/webmvc/RepositoryController.java | 54 ++- .../webmvc/RepositoryEntityController.java | 399 ++++++++---------- ...ormationHandlerMethodArgumentResolver.java | 31 +- ...RepositoryPropertyReferenceController.java | 229 +++++----- .../webmvc/RepositoryRestHandlerMapping.java | 22 +- .../rest/webmvc/RepositoryRestRequest.java | 83 +--- ...tRequestHandlerMethodArgumentResolver.java | 55 +-- .../webmvc/RepositorySearchController.java | 135 +++--- ...cessorHandlerMethodReturnValueHandler.java | 28 +- .../data/rest/webmvc/RestController.java | 35 ++ .../RepositoryRestMvcConfiguration.java | 382 ++++++++--------- .../webmvc}/json/Jackson2DatatypeHelper.java | 2 +- .../data/rest/webmvc}/json/JsonSchema.java | 2 +- .../json/PersistentEntityJackson2Module.java | 196 ++++----- ...PersistentEntityToJsonSchemaConverter.java | 43 +- .../data/rest/webmvc/json/package-info.java | 20 + .../webmvc/support/RepositoryEntityLinks.java | 150 +++---- .../webmvc/support/RepositoryLinkBuilder.java | 90 ++++ .../webmvc/AbstractWebIntegrationTests.java | 92 ++++ .../CustomMethodArgumentResolverTests.java | 45 +- .../data/rest/webmvc/Requests.java | 2 +- ...dlerMethodReturnValueHandlerUnitTests.java | 48 ++- ...ryRestMvConfigurationIntegrationTests.java | 60 +++ .../rest/webmvc/jpa/JpaRepositoryConfig.java | 37 +- .../data/rest/webmvc/jpa/JpaWebTests.java | 48 +++ .../PersistentEntitySerializationTests.java | 59 +-- .../webmvc/json/RepositoryTestsConfig.java | 82 ++++ .../src/test/resources/logback.xml | 16 + 73 files changed, 3168 insertions(+), 1820 deletions(-) delete mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResource.java delete mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResources.java delete mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingAndSorting.java delete mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoriesFactoryBean.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/CollectionResourceMapping.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/CollectionResourceMappingBuilder.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/InternalMappingBuilder.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/MappingBuilder.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceInformation.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryMethodResourceMapping.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMapping.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappingFactory.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappings.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadata.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadataProvider.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/SimpleCollectionResourceMapping.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoriesUtils.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryRelProvider.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/SimpleRelProvider.java create mode 100644 spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceMappingFactoryUnitTests.java create mode 100644 spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/ResourceMappingFactoryUnitTests.java create mode 100644 spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/ResourceMappingsIntegrationTest.java create mode 100644 spring-data-rest-repository/src/test/resources/logback.xml create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java delete mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestController.java rename {spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository => spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc}/json/Jackson2DatatypeHelper.java (96%) rename {spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository => spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc}/json/JsonSchema.java (97%) rename {spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository => spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc}/json/PersistentEntityJackson2Module.java (62%) rename {spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository => spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc}/json/PersistentEntityToJsonSchemaConverter.java (75%) create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/package-info.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java rename {spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository => spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc}/json/PersistentEntitySerializationTests.java (58%) create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java create mode 100644 spring-data-rest-webmvc/src/test/resources/logback.xml diff --git a/.gitignore b/.gitignore index 549b071d5..7a8023634 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ build/ .project .settings *.log +vf.gf.* \ No newline at end of file diff --git a/build.gradle b/build.gradle index d6141bf1c..282ea426e 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ ext { // Spring springVersion = "3.2.3.RELEASE" - hateoasVersion = "0.6.0.RELEASE" + hateoasVersion = "0.7.0.BUILD-SNAPSHOT" springPluginVersion = "0.8.0.RELEASE" springSecurityVersion = "3.1.3.RELEASE" sdCommonsVersion = "1.6.0.BUILD-SNAPSHOT" @@ -67,11 +67,11 @@ configure(allprojects) { sourceSets.test.resources.srcDirs = ["src/test/resources", "src/test/java"] repositories { + mavenLocal() + mavenCentral() maven { url "http://oss.sonatype.org/content/repositories/releases" } maven { url "http://m2.neo4j.org/releases" } - mavenLocal() maven { url "http://repo.springsource.org/libs-snapshot" } - mavenCentral() } dependencies { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/RepositoryRestConfiguration.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/RepositoryRestConfiguration.java index c080d804c..e1e6305f0 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/RepositoryRestConfiguration.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.config; import java.net.URI; @@ -10,7 +25,9 @@ import org.springframework.util.Assert; /** * @author Jon Brisbin + * @author Oliver Gierke */ +@SuppressWarnings("deprecation") public class RepositoryRestConfiguration { private URI baseUri = null; @@ -340,5 +357,4 @@ public class RepositoryRestConfiguration { Collections.addAll(exposeIdsFor, domainTypes); return this; } - } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMapping.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMapping.java index d43a3b0e7..a799727d3 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMapping.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMapping.java @@ -1,3 +1,18 @@ +/* + * 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.config; import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; @@ -8,6 +23,7 @@ import java.util.Map; /** * @author Jon Brisbin */ +@Deprecated public class ResourceMapping { private String rel; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMappingConfiguration.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMappingConfiguration.java index 38d24c34e..4d2d7ac9c 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMappingConfiguration.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/config/ResourceMappingConfiguration.java @@ -1,3 +1,18 @@ +/* + * 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.config; import java.util.HashMap; @@ -9,6 +24,7 @@ import java.util.Map; * * @author Jon Brisbin */ +@SuppressWarnings("deprecation") public class ResourceMappingConfiguration { private final Map, ResourceMapping> resourceMappings = new HashMap, ResourceMapping>(); diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResource.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResource.java deleted file mode 100644 index 1aafcc28e..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResource.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.springframework.data.rest.repository; - -import static org.springframework.data.rest.core.util.UriUtils.*; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; - -/** - * @author Jon Brisbin - */ -public class BaseUriAwareResource extends Resource { - - @JsonIgnore - private URI baseUri; - - public BaseUriAwareResource() { - } - - public BaseUriAwareResource(T content, Link... links) { - super(content, links); - } - - public BaseUriAwareResource(T content, Iterable links) { - super(content, links); - } - - public URI getBaseUri() { - return baseUri; - } - - public BaseUriAwareResource setBaseUri(URI baseUri) { - this.baseUri = baseUri; - return this; - } - - @Override public List getLinks() { - String baseUriStr = baseUri.toString(); - List links = new ArrayList(); - for(Link l : super.getLinks()) { - if(!l.getHref().startsWith(baseUriStr) - && !l.getHref().startsWith("http")) { - links.add(new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel())); - } else { - links.add(l); - } - } - return links; - } - - @Override public Link getLink(String rel) { - Link l = super.getLink(rel); - if(null == l) { - return null; - } - if(!l.getHref().startsWith(baseUri.toString()) - && !l.getHref().startsWith("http")) { - return new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel()); - } else { - return l; - } - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResources.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResources.java deleted file mode 100644 index 7785403f1..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/BaseUriAwareResources.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.springframework.data.rest.repository; - -import static org.springframework.data.rest.core.util.UriUtils.*; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.Resource; -import org.springframework.hateoas.Resources; - -/** - * @author Jon Brisbin - */ -public class BaseUriAwareResources extends Resources> { - - @JsonIgnore - private URI baseUri; - - public BaseUriAwareResources() { - } - - public BaseUriAwareResources(Iterable> content, Link... links) { - super(content, links); - } - - public BaseUriAwareResources(Iterable> content, Iterable links) { - super(content, links); - } - - public URI getBaseUri() { - return baseUri; - } - - public BaseUriAwareResources setBaseUri(URI baseUri) { - this.baseUri = baseUri; - return this; - } - - @Override public Collection> getContent() { - List> resources = new ArrayList>(); - for(Resource resource : super.getContent()) { - if(resource instanceof BaseUriAwareResource) { - resources.add(((BaseUriAwareResource) resource).setBaseUri(baseUri)); - } else { - resources.add(new BaseUriAwareResource(resource.getContent(), resource.getLinks()).setBaseUri(baseUri)); - } - } - return resources; - } - - @Override public Iterator> iterator() { - return getContent().iterator(); - } - - @Override public List getLinks() { - List links = new ArrayList(); - for(Link l : super.getLinks()) { - links.add(new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel())); - } - return links; - } - - @Override public Link getLink(String rel) { - Link l = super.getLink(rel); - return new Link(buildUri(baseUri, l.getHref()).toString(), l.getRel()); - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingAndSorting.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingAndSorting.java deleted file mode 100644 index 3d34d71e3..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PagingAndSorting.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.springframework.data.rest.repository; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Iterator; - -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.rest.config.RepositoryRestConfiguration; -import org.springframework.web.util.UriComponentsBuilder; - -/** - * Implementation of {@link Pageable} that is URL-aware. - * - * @author Jon Brisbin - */ -public class PagingAndSorting implements Pageable { - - private final RepositoryRestConfiguration config; - private final PageRequest pageRequest; - - public PagingAndSorting(RepositoryRestConfiguration config, - PageRequest pageRequest) { - this.config = config; - this.pageRequest = pageRequest; - } - - /** - * Add the current sort parameters to the URI. - * - * @param urib - * - * @return - */ - public PagingAndSorting addSortParameters(UriComponentsBuilder urib) { - Sort sort = pageRequest.getSort(); - if(null != sort) { - Iterator iter = sort.iterator(); - while(iter.hasNext()) { - Sort.Order order = iter.next(); - urib.queryParam(config.getSortParamName(), order.getProperty()); - try { - urib.queryParam(URLEncoder.encode(order.getProperty() + ".dir", "ISO-8859-1"), - order.getDirection().toString().toLowerCase()); - } catch(UnsupportedEncodingException ignored) { - // this should never happen - } - } - } - return this; - } - - @Override public int getPageNumber() { - return pageRequest.getPageNumber(); - } - - @Override public int getPageSize() { - return pageRequest.getPageSize(); - } - - @Override public int getOffset() { - return pageRequest.getOffset(); - } - - @Override public Sort getSort() { - return pageRequest.getSort(); - } - - @Override public Pageable first() { - return pageRequest.first(); - } - - @Override public Pageable next() { - return pageRequest.next(); - } - - @Override public boolean hasPrevious() { - return pageRequest.hasPrevious(); - } - - @Override public Pageable previousOrFirst() { - return pageRequest.previousOrFirst(); - } -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PersistentEntityResource.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PersistentEntityResource.java index f0c259ba7..81166b100 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PersistentEntityResource.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/PersistentEntityResource.java @@ -1,50 +1,51 @@ +/* + * Copyright 2012-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; -import java.net.URI; +import java.util.Arrays; -import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.data.mapping.PersistentEntity; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; +import com.fasterxml.jackson.annotation.JsonIgnore; + /** * A Spring HATEOAS {@link Resource} subclass that holds a reference to the entity's {@link PersistentEntity} metadata. - * + * * @author Jon Brisbin */ -public class PersistentEntityResource extends BaseUriAwareResource { +public class PersistentEntityResource extends Resource { - @JsonIgnore - private final PersistentEntity persistentEntity; + @JsonIgnore private final PersistentEntity entity; - public static PersistentEntityResource wrap(PersistentEntity persistentEntity, - T obj, - URI baseUri) { - PersistentEntityResource resource = new PersistentEntityResource(persistentEntity, obj); - resource.setBaseUri(baseUri); - return resource; - } + public static PersistentEntityResource wrap(PersistentEntity entity, T obj) { + return new PersistentEntityResource(entity, obj); + } - public PersistentEntityResource(PersistentEntity persistentEntity) { - this.persistentEntity = persistentEntity; - } + public PersistentEntityResource(PersistentEntity entity, T content, Link... links) { + this(entity, content, Arrays.asList(links)); + } - public PersistentEntityResource(PersistentEntity persistentEntity, - T content, - Link... links) { - super(content, links); - this.persistentEntity = persistentEntity; - } - - public PersistentEntityResource(PersistentEntity persistentEntity, - T content, - Iterable links) { - super(content, links); - this.persistentEntity = persistentEntity; - } - - public PersistentEntity getPersistentEntity() { - return persistentEntity; - } + private PersistentEntityResource(PersistentEntity entity, T content, Iterable links) { + super(content, links); + this.entity = entity; + } + public PersistentEntity getPersistentEntity() { + return entity; + } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoriesFactoryBean.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoriesFactoryBean.java deleted file mode 100644 index 4bd91f058..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoriesFactoryBean.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.springframework.data.rest.repository.context; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.data.repository.support.Repositories; - -/** - * @author Jon Brisbin - */ -public class RepositoriesFactoryBean implements FactoryBean, - ApplicationContextAware { - - private ApplicationContext applicationContext; - - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - @Override public Repositories getObject() throws Exception { - return new Repositories(applicationContext); - } - - @Override public Class getObjectType() { - return Repositories.class; - } - - @Override public boolean isSingleton() { - return false; - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java index 0a7e47368..3f35409ca 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java @@ -1,13 +1,15 @@ package org.springframework.data.rest.repository.invoke; -import static org.springframework.util.ReflectionUtils.doWithMethods; -import static org.springframework.util.ReflectionUtils.invokeMethod; +import static org.springframework.util.ReflectionUtils.*; import java.io.Serializable; import java.lang.reflect.Method; import java.util.HashMap; +import java.util.List; import java.util.Map; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -19,10 +21,13 @@ import org.springframework.util.ReflectionUtils.MethodCallback; /** * @author Jon Brisbin */ +@SuppressWarnings("deprecation") public class RepositoryMethodInvoker implements PagingAndSortingRepository { private final Object repository; private final Map queryMethods = new HashMap(); + private final ConversionService conversionService; + private RepositoryMethod saveOne; private RepositoryMethod saveSome; private RepositoryMethod findOne; @@ -38,8 +43,11 @@ public class RepositoryMethodInvoker implements PagingAndSortingRepository repoType = repoInfo.getRepositoryInterface(); doWithMethods(repoType, new MethodCallback() { @@ -219,5 +227,49 @@ public class RepositoryMethodInvoker implements PagingAndSortingRepository rawParameters) { + return invokeQueryMethod(method, foo(method, pageable, rawParameters)); + } + + private Object[] foo(RepositoryMethod repoMethod, Pageable pageable, Map rawParameters) { + + List methodParams = repoMethod.getParameters(); + + if (methodParams.isEmpty()) { + return new Object[0]; + } + + Object[] paramValues = new Object[methodParams.size()]; + + + for(int i = 0; i < paramValues.length; i++) { + MethodParameter param = methodParams.get(i); + Class targetType = param.getParameterType(); + if(Pageable.class.isAssignableFrom(targetType)) { + paramValues[i] = pageable; + } else if(Sort.class.isAssignableFrom(targetType)) { + paramValues[i] = pageable.getSort(); + } else { + String paramName = repoMethod.getParameterNames().get(i); + String[] queryParamVals = rawParameters.get(paramName); + if(null == queryParamVals) { + if(paramName.startsWith("arg")) { + throw new IllegalArgumentException("No @Param annotation found on query method " + + repoMethod.getMethod().getName() + + " for parameter " + param.getParameterName()); + } else { + throw new IllegalArgumentException("No query parameter specified for " + + repoMethod.getMethod().getName() + " param '" + + paramName + "'"); + } + } + paramValues[i] = conversionService.convert(queryParamVals, targetType); + } + } + + + return paramValues; + } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/CollectionResourceMapping.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/CollectionResourceMapping.java new file mode 100644 index 000000000..a91cd5e20 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/CollectionResourceMapping.java @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * A custom resource mapping for collection resources. + * + * @author Oliver Gierke + */ +public interface CollectionResourceMapping extends ResourceMapping { + + String getSingleResourceRel(); +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/CollectionResourceMappingBuilder.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/CollectionResourceMappingBuilder.java new file mode 100644 index 000000000..5dc8cd992 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/CollectionResourceMappingBuilder.java @@ -0,0 +1,115 @@ +/* + * 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 org.springframework.util.Assert; + + +class CollectionResourceMappingBuilder implements InternalMappingBuilder { + + private final CollectionResourceMapping mapping; + + public CollectionResourceMappingBuilder(CollectionResourceMapping mapping) { + this.mapping = mapping; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#withCollectionRel(java.lang.String) + */ + public CollectionResourceMappingBuilder withCollectionRel(String rel) { + + SimpleCollectionResourceMapping newMapping = new SimpleCollectionResourceMapping(rel != null ? rel : mapping.getRel(), + mapping.getSingleResourceRel(), mapping.getPath(), mapping.isExported()); + + return new CollectionResourceMappingBuilder(newMapping); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#withSingleRel(java.lang.String) + */ + @Override + public CollectionResourceMappingBuilder withSingleRel(String rel) { + + SimpleCollectionResourceMapping newMapping = new SimpleCollectionResourceMapping(mapping.getRel(), + rel != null ? rel : mapping.getSingleResourceRel(), mapping.getPath(), mapping.isExported()); + + return new CollectionResourceMappingBuilder(newMapping); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#withPath(java.lang.String) + */ + @Override + public CollectionResourceMappingBuilder withPath(String path) { + + SimpleCollectionResourceMapping newMapping = new SimpleCollectionResourceMapping(mapping.getRel(), + mapping.getSingleResourceRel(), path != null ? path : mapping.getPath(), mapping.isExported()); + + return new CollectionResourceMappingBuilder(newMapping); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#withExposed(java.lang.Boolean) + */ + @Override + public CollectionResourceMappingBuilder withExposed(Boolean exported) { + + SimpleCollectionResourceMapping newMapping = new SimpleCollectionResourceMapping(mapping.getRel(), + mapping.getSingleResourceRel(), mapping.getPath(), exported != null ? exported : mapping.isExported()); + + return new CollectionResourceMappingBuilder(newMapping); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#merge(org.springframework.data.rest.repository.mapping.CollectionResourceMapping) + */ + @Override + public InternalMappingBuilder merge(CollectionResourceMapping mapping) { + + if (mapping == null) { + return this; + } + + return withCollectionRel(mapping.getRel()). // + withSingleRel(mapping.getSingleResourceRel()). // + withPath(mapping.getPath()). // + withExposed(mapping.isExported()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.InternalMappingBuilder#getMapping() + */ + @Override + public CollectionResourceMapping getMapping() { + + Assert.hasText(mapping.getRel(), "Rel must not be null or empty!"); + + return mapping; + } + + /** + * @return the exported + */ + public Boolean isExported() { + return mapping.isExported(); + } +} \ No newline at end of file diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/InternalMappingBuilder.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/InternalMappingBuilder.java new file mode 100644 index 000000000..42f4b422b --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/InternalMappingBuilder.java @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * @author Oliver Gierke + */ +interface InternalMappingBuilder extends MappingBuilder { + + InternalMappingBuilder merge(CollectionResourceMapping mapping); + + CollectionResourceMapping getMapping(); +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/MappingBuilder.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/MappingBuilder.java new file mode 100644 index 000000000..b7c33a985 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/MappingBuilder.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; + +/** + * SPI to allow users to register + * + * @author Oliver Gierke + */ +public interface MappingBuilder { + + MappingBuilder withCollectionRel(String rel); + + MappingBuilder withSingleRel(String rel); + + MappingBuilder withPath(String path); + + MappingBuilder withExposed(Boolean exposed); + +} 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 new file mode 100644 index 000000000..d42753c8f --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceInformation.java @@ -0,0 +1,119 @@ +/* + * 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 org.springframework.context.annotation.Primary; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.util.Assert; + +/** + * @author Oliver Gierke + */ +public class RepositoryAwareResourceInformation implements ResourceMetadata { + + private final Repositories repositories; + private final CollectionResourceMapping mapping; + private final ResourceMetadataProvider provider; + private final RepositoryInformation repositoryInterface; + + /** + * @param repositories must not be {@literal null}. + * @param mapping must not be {@literal null}. + * @param provider must not be {@literal null}. + */ + public RepositoryAwareResourceInformation(Repositories repositories, CollectionResourceMapping mapping, + ResourceMetadataProvider provider, RepositoryInformation repositoryInterface) { + + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(mapping, "ResourceMapping must not be null!"); + Assert.notNull(provider, "ResourceMetadataProvider must not be null!"); + + this.repositories = repositories; + this.mapping = mapping; + this.provider = provider; + this.repositoryInterface = repositoryInterface; + } + + public boolean isPrimary() { + return AnnotationUtils.findAnnotation(repositoryInterface.getRepositoryInterface(), Primary.class) != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.DelegatingResourceInformation#isManaged(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public boolean isManaged(PersistentProperty property) { + return repositories.hasRepositoryFor(property.getActualType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMetadataProvider#getMappingFor(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public ResourceMapping getMappingFor(PersistentProperty property) { + return provider.getMappingFor(property); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMetadataProvider#hasMappingFor(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public boolean isMapped(PersistentProperty property) { + return provider.isMapped(property); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#isExported() + */ + @Override + public Boolean isExported() { + return mapping.isExported(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getCollectionRel() + */ + @Override + public String getRel() { + return mapping.getRel(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getSingleResourceRel() + */ + @Override + public String getSingleResourceRel() { + return mapping.getSingleResourceRel(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getPath() + */ + @Override + public String getPath() { + return mapping.getPath(); + } +} 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 new file mode 100644 index 000000000..1638d933b --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/RepositoryMethodResourceMapping.java @@ -0,0 +1,74 @@ +/* + * 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 org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.rest.repository.annotation.RestResource; + +/** + * A {@link RepositoryMethodResourceMapping} created from a {@link Method}. + * + * @author Oliver Gierke + */ +public class RepositoryMethodResourceMapping implements ResourceMapping { + + private final boolean isExported; + private final String rel; + private final String path; + + /** + * Creates a new {@link RepositoryMethodResourceMapping} for the given {@link Method}. + * + * @param method must not be {@literal null}. + */ + public RepositoryMethodResourceMapping(Method method) { + + RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class); + + this.isExported = annotation != null ? annotation.exported() : true; + this.rel = annotation != null ? annotation.rel() : method.getName(); + this.path = annotation != null ? annotation.path() : method.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMapping#isExported() + */ + @Override + public Boolean isExported() { + return isExported; + } + + /* + * (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#getPath() + */ + @Override + public String getPath() { + return path; + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMapping.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMapping.java new file mode 100644 index 000000000..f93e0c3a3 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMapping.java @@ -0,0 +1,63 @@ +/* + * 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; + +/** + * Mapping information for components to be exported as REST resources. + * + * @author Oliver Gierke + */ +public interface ResourceMapping { + + public static ResourceMapping NO_MAPPING = new ResourceMapping() { + + @Override + public Boolean isExported() { + return false; + } + + @Override + public String getRel() { + return null; + } + + @Override + public String getPath() { + return null; + } + }; + + /** + * Returns whether the component shall be exported at all. + * + * @return will never be {@literal null}. + */ + Boolean isExported(); + + /** + * Returns the relation for the resource exported. + * + * @return will never be {@literal null}. + */ + String getRel(); + + /** + * Returns the path the resource is exposed under. + * + * @return will never be {@literal null}. + */ + String getPath(); +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappingFactory.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappingFactory.java new file mode 100644 index 000000000..0eb26b9eb --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappingFactory.java @@ -0,0 +1,144 @@ +/* + * 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 org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.rest.repository.annotation.RestResource; +import org.springframework.data.rest.repository.support.RepositoriesUtils; +import org.springframework.hateoas.RelProvider; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Oliver Gierke + */ +public class ResourceMappingFactory { + + private final RelProvider relProvider; + + public ResourceMappingFactory(RelProvider relProvider) { + this.relProvider = relProvider; + } + + public CollectionResourceMapping getMappingForType(Class type) { + return getMappingForType(type, new CollectionResourceMapping[0]); + } + + public CollectionResourceMapping getMappingForType(Class type, CollectionResourceMapping... manualMapping) { + + Class typeToInspect = getTypeToInspect(type); + + InternalMappingBuilder mapping = getBaseMetadata(typeToInspect). // + merge(discoverConfig(typeToInspect)); + + if (type != typeToInspect) { + mapping = mapping.merge(discoverConfig(type)); + } + + for (CollectionResourceMapping externalMapping : manualMapping) { + mapping = mapping.merge(externalMapping); + } + + return mapping.getMapping(); + } + + private static Class getTypeToInspect(Class type) { + + if (!RepositoriesUtils.isRepositoryInterface(type)) { + return type; + } + + return RepositoriesUtils.getDomainType(type); + } + + private InternalMappingBuilder getBaseMetadata(Class domainType) { + + String path = StringUtils.uncapitalize(domainType.getSimpleName()); + String defaultCollectionRel = relProvider.getCollectionResourceRelFor(domainType); + String defaultSingleRel = relProvider.getSingleResourceRelFor(domainType); + + CollectionResourceMapping mapping = new SimpleCollectionResourceMapping(defaultCollectionRel, defaultSingleRel, path, true); + + return new CollectionResourceMappingBuilder(mapping); + } + + private static final CollectionResourceMapping discoverConfig(Class type) { + + RestResource resource = AnnotationUtils.findAnnotation(type, RestResource.class); + + if (resource == null) { + return null; + } + + return new AnnotationResourceMapping(resource); + } + + /** + * {@link CollectionResourceMapping} based on an {@link RestResource} annotation. + * + * @author Oliver Gierke + */ + private static class AnnotationResourceMapping implements CollectionResourceMapping { + + private final RestResource annotation; + + /** + * Creates a new {@link AnnotationResourceMapping} for the given {@link RestResource}. + * + * @param annotation must not be {@literal null}. + */ + public AnnotationResourceMapping(RestResource annotation) { + Assert.notNull(annotation, "Annotation must not be null!"); + this.annotation = annotation; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#isExported() + */ + @Override + public Boolean isExported() { + return annotation.exported(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getCollectionRel() + */ + @Override + public String getRel() { + return StringUtils.hasText(annotation.rel()) ? annotation.rel() : null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getSingleResourceRel() + */ + @Override + public String getSingleResourceRel() { + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getPath() + */ + @Override + public String getPath() { + return StringUtils.hasText(annotation.path()) ? annotation.path() : null; + } + } +} 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 new file mode 100644 index 000000000..4ca06ef2c --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMappings.java @@ -0,0 +1,215 @@ +/* + * 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.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.springframework.data.mapping.PersistentProperty; +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.support.RepositoriesUtils; +import org.springframework.data.rest.repository.support.SimpleRelProvider; +import org.springframework.hateoas.RelProvider; +import org.springframework.util.Assert; + +/** + * Central abstraction obtain {@link ResourceMetadata} and {@link ResourceMapping} instances for domain types and + * repositories. + * + * @author Oliver Gierke + */ +@SuppressWarnings("deprecation") +public class ResourceMappings implements ResourceMetadataProvider, Iterable { + + private final RepositoryRestConfiguration config; + private final ResourceMappingFactory factory; + private final Repositories repositories; + + private final Map, ResourceMetadata> cache = new HashMap, ResourceMetadata>(); + private final Map, Map> searchCache = new HashMap, Map>(); + + /** + * Creates a new {@link ResourceMappings} using the given {@link RepositoryRestConfiguration} and {@link Repositories} + * . + * + * @param config + * @param repositories + */ + public ResourceMappings(RepositoryRestConfiguration config, Repositories repositories) { + this(config, repositories, new SimpleRelProvider()); + } + + /** + * Creates a new {@link ResourceMappings} from the given {@link RepositoryRestConfiguration}, {@link Repositories} and + * {@link RelProvider}. + * + * @param config must not be {@literal null}. + * @param repositories must not be {@literal null}. + * @param relProvider must not be {@literal null}. + */ + public ResourceMappings(RepositoryRestConfiguration config, Repositories repositories, RelProvider relProvider) { + + Assert.notNull(config, "RepositoryRestConfiguration must not be null!"); + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(relProvider, "RelProvider must not be null!"); + + this.config = config; + this.repositories = repositories; + this.factory = new ResourceMappingFactory(relProvider); + + this.populateCache(repositories); + } + + /** + * Returns a {@link ResourceMetadata} for the given type if available. + * + * @param type must not be {@literal null}. + * @return + */ + public ResourceMetadata getMappingFor(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return cache.get(type); + } + + private final void populateCache(Repositories repositories) { + + for (Class type : repositories) { + + RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(type); + Class repositoryInterface = repositoryInformation.getRepositoryInterface(); + + CollectionResourceMapping mapping = factory.getMappingForType(repositoryInterface, fromConfig(type), + fromConfig(repositoryInterface)); + + RepositoryAwareResourceInformation information = new RepositoryAwareResourceInformation(repositories, mapping, + this, repositoryInformation); + + cache.put(repositoryInterface, information); + + if (!cache.containsKey(type) || information.isPrimary()) { + cache.put(type, information); + } + } + } + + /** + * Returns the {@link ResourceMapping}s for the search resources of the given type. + * + * @param type + * @return + */ + public Map getSearchResourceMappings(Class type) { + + if (searchCache.containsKey(type)) { + return searchCache.get(type); + } + + Class domainType = RepositoriesUtils.getDomainType(type); + RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(domainType); + Map mappings = new HashMap(); + + for (Method queryMethod : repositoryInformation.getQueryMethods()) { + mappings.put(queryMethod.getName(), new RepositoryMethodResourceMapping(queryMethod)); + } + + searchCache.put(type, mappings); + return mappings; + } + + /** + * Returns whether we have a {@link ResourceMapping} for the given type and it is exported. + * + * @param type + * @return + */ + public boolean exportsMappingFor(Class type) { + + if (!hasMappingFor(type)) { + return false; + } + + ResourceMetadata metadata = getMappingFor(type); + return metadata.isExported(); + } + + /** + * Returns whether we have a {@link ResourceMapping} for the given type. + * + * @param type must not be {@literal null}. + * @return + */ + public boolean hasMappingFor(Class type) { + + if (cache.containsKey(type)) { + return true; + } + + if (repositories.hasRepositoryFor(type)) { + return true; + } + + if (RepositoriesUtils.isRepositoryInterface(type) && hasMappingFor(RepositoriesUtils.getDomainType(type))) { + return true; + } + + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMetadataProvider#getMappingFor(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public ResourceMapping getMappingFor(PersistentProperty property) { + return getMappingFor(property.getActualType()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.ResourceMetadataProvider#hasMappingFor(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public boolean isMapped(PersistentProperty property) { + + ResourceMapping metadata = getMappingFor(property); + return metadata != null && metadata.isExported(); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return cache.values().iterator(); + } + + private CollectionResourceMapping fromConfig(Class domainType) { + + org.springframework.data.rest.config.ResourceMapping mapping = config.getResourceMappingForDomainType(domainType); + + if (mapping == null) { + return null; + } + + return new SimpleCollectionResourceMapping(mapping.getRel(), null, mapping.getPath(), mapping.isExported()); + } +} 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 new file mode 100644 index 000000000..c93a9ca53 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadata.java @@ -0,0 +1,27 @@ +/* + * 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 org.springframework.data.mapping.PersistentProperty; + +/** + * + * @author Oliver Gierke + */ +public interface ResourceMetadata extends CollectionResourceMapping, ResourceMetadataProvider { + + boolean isManaged(PersistentProperty property); +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadataProvider.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadataProvider.java new file mode 100644 index 000000000..ec3dedb08 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/ResourceMetadataProvider.java @@ -0,0 +1,29 @@ +/* + * 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 org.springframework.data.mapping.PersistentProperty; + +/** + * + * @author Oliver Gierke + */ +public interface ResourceMetadataProvider { + + boolean isMapped(PersistentProperty property); + + ResourceMapping getMappingFor(PersistentProperty property); +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/SimpleCollectionResourceMapping.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/SimpleCollectionResourceMapping.java new file mode 100644 index 000000000..2f3d5227e --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/mapping/SimpleCollectionResourceMapping.java @@ -0,0 +1,69 @@ +/* + * 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; + + +public class SimpleCollectionResourceMapping implements CollectionResourceMapping { + + private final String collectionRel; + private final String singleRel; + private final String path; + private final Boolean exported; + + public SimpleCollectionResourceMapping(String relsAndPath) { + this(relsAndPath, relsAndPath, relsAndPath, true); + } + + public SimpleCollectionResourceMapping(String collectionRel, String singleRel, String path, Boolean exported) { + + this.collectionRel = collectionRel; + this.singleRel = singleRel; + this.path = path; + this.exported = exported; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getCollectionRel() + */ + public String getRel() { + return collectionRel; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getSingleResourceRel() + */ + public String getSingleResourceRel() { + return singleRel; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.mapping.CollectionResourceMapping#getPath() + */ + @Override + public String getPath() { + return path; + } + + /** + * @return the exported + */ + public Boolean isExported() { + return exported; + } +} \ No newline at end of file diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoriesUtils.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoriesUtils.java new file mode 100644 index 000000000..c9d399b67 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoriesUtils.java @@ -0,0 +1,49 @@ +/* + * 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.support; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.RepositoryDefinition; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * + * @author Oliver Gierke + */ +public class RepositoriesUtils { + + public static Class getDomainType(Class repositoryType) { + + if (!isRepositoryInterface(repositoryType)) { + return null; + } + + return getMetadataFor(repositoryType).getDomainType(); + } + + public static boolean isRepositoryInterface(Class type) { + return Repository.class.isAssignableFrom(type) + || AnnotationUtils.findAnnotation(type, RepositoryDefinition.class) != null; + } + + private static RepositoryMetadata getMetadataFor(Class type) { + return Repository.class.isAssignableFrom(type) ? new DefaultRepositoryMetadata(type) + : new AnnotationRepositoryMetadata(type); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryInformationSupport.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryInformationSupport.java index 8c21b978a..54fe4c215 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryInformationSupport.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryInformationSupport.java @@ -19,6 +19,7 @@ import org.springframework.util.MultiValueMap; /** * @author Jon Brisbin */ +@SuppressWarnings("deprecation") public abstract class RepositoryInformationSupport { protected Repositories repositories; @@ -57,7 +58,7 @@ public abstract class RepositoryInformationSupport { } for(Class domainType : repositories) { RepositoryInformation repoInfo = findRepositoryInfoFor(domainType); - ResourceMapping mapping = getResourceMapping(config, repoInfo); + ResourceMapping mapping = getResourceMapping(config, repoInfo); if(pathSegment.equals(mapping.getPath()) && mapping.isExported()) { return repoInfo; } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryRelProvider.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryRelProvider.java new file mode 100644 index 000000000..f24efed1f --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/RepositoryRelProvider.java @@ -0,0 +1,64 @@ +/* + * 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.support; + +import org.springframework.data.rest.repository.mapping.ResourceMappings; +import org.springframework.hateoas.RelProvider; + +/** + * + * @author Oliver Gierke + */ +public class RepositoryRelProvider implements RelProvider { + + private final ResourceMappings mappings; + + /** + * @param repositories + * @param config + */ + public RepositoryRelProvider(ResourceMappings mappings) { + + this.mappings = mappings; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getCollectionResourceRelFor(java.lang.Class) + */ + @Override + public String getCollectionResourceRelFor(Class type) { + return mappings.getMappingFor(type).getRel(); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getSingleResourceRelFor(java.lang.Class) + */ + @Override + public String getSingleResourceRelFor(Class type) { + return mappings.getMappingFor(type).getSingleResourceRel(); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return mappings.hasMappingFor(delimiter); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/ResourceMappingUtils.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/ResourceMappingUtils.java index cfd61241e..251017756 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/ResourceMappingUtils.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/ResourceMappingUtils.java @@ -17,6 +17,7 @@ import org.springframework.data.rest.repository.annotation.RestResource; * * @author Jon Brisbin */ +@Deprecated public abstract class ResourceMappingUtils { protected ResourceMappingUtils() { diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/SimpleRelProvider.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/SimpleRelProvider.java new file mode 100644 index 000000000..162d59964 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/support/SimpleRelProvider.java @@ -0,0 +1,53 @@ +/* + * 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.support; + +import org.springframework.hateoas.RelProvider; +import org.springframework.util.StringUtils; + +/** + * + * @author Oliver Gierke + */ +public class SimpleRelProvider implements RelProvider { + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return true; + } + + /* (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getSingleResourceRelFor(java.lang.Class) + */ + @Override + public String getSingleResourceRelFor(Class type) { + String collectionRel = getCollectionResourceRelFor(type); + return String.format("%s.%s", collectionRel, collectionRel); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.RelProvider#getCollectionResourceRelFor(java.lang.Class) + */ + @Override + public String getCollectionResourceRelFor(Class type) { + return StringUtils.uncapitalize(type.getSimpleName()); + } +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/config/ResourceMappingUnitTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/config/ResourceMappingUnitTests.java index b97adee34..8adfbedf4 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/config/ResourceMappingUnitTests.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/config/ResourceMappingUnitTests.java @@ -2,15 +2,13 @@ package org.springframework.data.rest.config; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.Matchers.*; -import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; - -import java.lang.reflect.Method; import org.junit.Test; -import org.springframework.data.domain.Pageable; import org.springframework.data.rest.repository.domain.jpa.AnnotatedPersonRepository; -import org.springframework.data.rest.repository.domain.jpa.PersonRepository; import org.springframework.data.rest.repository.domain.jpa.PlainPersonRepository; +import org.springframework.data.rest.repository.mapping.ResourceMapping; +import org.springframework.data.rest.repository.mapping.ResourceMappingFactory; +import org.springframework.data.rest.repository.support.SimpleRelProvider; /** * Ensure the {@link ResourceMapping} components convey the correct information. @@ -18,46 +16,26 @@ import org.springframework.data.rest.repository.domain.jpa.PlainPersonRepository * @author Jon Brisbin */ public class ResourceMappingUnitTests { + + ResourceMappingFactory factory = new ResourceMappingFactory(new SimpleRelProvider()); @Test public void shouldDetectDefaultRelAndPath() throws Exception { - ResourceMapping mapping = new ResourceMapping( - findRel(PlainPersonRepository.class), - findPath(PlainPersonRepository.class), - findExported(PlainPersonRepository.class) - ); - - assertThat(mapping.getRel(), is("plainPerson")); - assertThat(mapping.getPath(), is("plainPerson")); - assertThat(mapping.isExported(), is(true)); + + ResourceMapping newMapping = factory.getMappingForType(PlainPersonRepository.class); + + assertThat(newMapping.getRel(), is("person")); + assertThat(newMapping.getPath(), is("person")); + assertThat(newMapping.isExported(), is(true)); } @Test public void shouldDetectAnnotatedRelAndPath() throws Exception { - ResourceMapping mapping = new ResourceMapping( - findRel(AnnotatedPersonRepository.class), - findPath(AnnotatedPersonRepository.class), - findExported(AnnotatedPersonRepository.class) - ); - - assertThat(mapping.getRel(), is("people")); - // The path is not set on the annotation so this should be the default from class name. - assertThat(mapping.getPath(), is("annotatedPerson")); - assertThat(mapping.isExported(), is(false)); + + ResourceMapping newMapping = factory.getMappingForType(AnnotatedPersonRepository.class); + + assertThat(newMapping.getRel(), is("people")); + assertThat(newMapping.getPath(), is("person")); + assertThat(newMapping.isExported(), is(false)); } - - @Test - public void shouldDetectAnnotatedRelAndPathOnMethod() throws Exception { - Method method = PersonRepository.class.getMethod("findByFirstName", String.class, Pageable.class); - ResourceMapping mapping = new ResourceMapping( - findRel(method), - findPath(method), - findExported(method) - ); - - assertThat(mapping.getRel(), is("firstname")); - assertThat(mapping.getPath(), is("firstname")); - assertThat(mapping.isExported(), is(true)); - } - } diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryRestConfigurationIntegrationTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryRestConfigurationIntegrationTests.java index 853bfaeaf..9f333deb7 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryRestConfigurationIntegrationTests.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryRestConfigurationIntegrationTests.java @@ -19,6 +19,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = RepositoryTestsConfig.class) +@SuppressWarnings("deprecation") public class RepositoryRestConfigurationIntegrationTests { @Autowired @@ -26,7 +27,7 @@ public class RepositoryRestConfigurationIntegrationTests { @Test public void shouldProvideResourceMappingForConfiguredRepository() throws Exception { - ResourceMapping mapping = config.getResourceMappingForRepository(ConfiguredPersonRepository.class); + ResourceMapping mapping = config.getResourceMappingForRepository(ConfiguredPersonRepository.class); assertThat(mapping, notNullValue()); assertThat(mapping.getRel(), is("people")); diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryTestsConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryTestsConfig.java index 1be44b7c8..abb8da9f4 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryTestsConfig.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/RepositoryTestsConfig.java @@ -1,7 +1,5 @@ package org.springframework.data.rest.repository; -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -14,7 +12,6 @@ import org.springframework.data.rest.repository.domain.jpa.ConfiguredPersonRepos import org.springframework.data.rest.repository.domain.jpa.JpaRepositoryConfig; import org.springframework.data.rest.repository.domain.jpa.Person; import org.springframework.data.rest.repository.domain.jpa.PersonRepository; -import org.springframework.data.rest.repository.json.PersistentEntityJackson2Module; import org.springframework.format.support.DefaultFormattingConversionService; /** @@ -31,7 +28,8 @@ public class RepositoryTestsConfig { return new Repositories(appCtx); } - @Bean public RepositoryRestConfiguration config() { + @SuppressWarnings("deprecation") + @Bean public RepositoryRestConfiguration config() { RepositoryRestConfiguration config = new RepositoryRestConfiguration(); config.setResourceMappingForDomainType(Person.class) @@ -63,15 +61,4 @@ public class RepositoryTestsConfig { @Bean public UriDomainClassConverter uriDomainClassConverter() { return new UriDomainClassConverter(); } - - @Bean public Module persistentEntityModule() { - return new PersistentEntityJackson2Module(defaultConversionService()); - } - - @Bean public ObjectMapper objectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(persistentEntityModule()); - return mapper; - } - } diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/ConfiguredPersonRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/ConfiguredPersonRepository.java index b95178388..19d6b8b5d 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/ConfiguredPersonRepository.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/ConfiguredPersonRepository.java @@ -1,11 +1,13 @@ package org.springframework.data.rest.repository.domain.jpa; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.NoRepositoryBean; /** * A repository to manage {@link Person}s. * * @author Jon Brisbin */ +@NoRepositoryBean public interface ConfiguredPersonRepository extends CrudRepository { } diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/JpaRepositoryConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/JpaRepositoryConfig.java index 467f919b0..22513ef2b 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/JpaRepositoryConfig.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/JpaRepositoryConfig.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.domain.jpa; import javax.persistence.EntityManagerFactory; @@ -22,9 +37,10 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author Jon Brisbin + * @author Oliver Gierke */ @Configuration -@ComponentScan(basePackageClasses = {JpaRepositoryConfig.class}) +@ComponentScan @EnableJpaRepositories @EnableTransactionManagement public class JpaRepositoryConfig { diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonLoader.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonLoader.java index 5f85fb70e..d87ffc707 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonLoader.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PersonLoader.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Component; public class PersonLoader implements InitializingBean { @Autowired - private PlainPersonRepository people; + private AnnotatedPersonRepository people; @Override public void afterPropertiesSet() throws Exception { people.save(new Person("John", "Doe")); diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PlainPersonRepository.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PlainPersonRepository.java index 624808b69..f9d49feda 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PlainPersonRepository.java +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/domain/jpa/PlainPersonRepository.java @@ -1,11 +1,13 @@ package org.springframework.data.rest.repository.domain.jpa; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.NoRepositoryBean; /** * A repository to manage {@link Person}s. * * @author Jon Brisbin */ +@NoRepositoryBean public interface PlainPersonRepository extends CrudRepository { } diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceMappingFactoryUnitTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceMappingFactoryUnitTests.java new file mode 100644 index 000000000..bb20dbb96 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/RepositoryAwareResourceMappingFactoryUnitTests.java @@ -0,0 +1,60 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.rest.repository.annotation.RestResource; +import org.springframework.data.rest.repository.support.SimpleRelProvider; + +/** + * + * @author Oliver Gierke + */ +public class RepositoryAwareResourceMappingFactoryUnitTests { + + ResourceMappingFactory factory = new ResourceMappingFactory(new SimpleRelProvider()); + + @Test + public void foo() { + + CollectionResourceMapping mapping = factory.getMappingForType(Person.class); + assertThat(mapping.getPath(), is("person")); + assertThat(mapping.getRel(), is("person")); + assertThat(mapping.getSingleResourceRel(), is("person.person")); + } + + @Test + public void honorsAnnotatedMapping() { + + CollectionResourceMapping mapping = factory.getMappingForType(AnnotatedPerson.class); + assertThat(mapping.getPath(), is("bar")); + assertThat(mapping.getRel(), is("foo")); + assertThat(mapping.getSingleResourceRel(), is("foo.foo")); + assertThat(mapping.isExported(), is(false)); + } + + static class Person { + + } + + @RestResource(path = "bar", rel = "foo", exported = false) + static class AnnotatedPerson { + + } +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/ResourceMappingFactoryUnitTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/ResourceMappingFactoryUnitTests.java new file mode 100644 index 000000000..a6ec726d4 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/ResourceMappingFactoryUnitTests.java @@ -0,0 +1,90 @@ +/* + * 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.repository.Repository; +import org.springframework.data.rest.repository.annotation.RestResource; +import org.springframework.data.rest.repository.support.SimpleRelProvider; + +/** + * + * @author Oliver Gierke + */ +public class ResourceMappingFactoryUnitTests { + + ResourceMappingFactory factory = new ResourceMappingFactory(new SimpleRelProvider()); + + @Test + public void foo() { + + CollectionResourceMapping mapping = factory.getMappingForType(Person.class); + assertThat(mapping.getPath(), is("person")); + assertThat(mapping.getRel(), is("person")); + assertThat(mapping.getSingleResourceRel(), is("person.person")); + } + + @Test + public void honorsAnnotatedMapping() { + + CollectionResourceMapping mapping = factory.getMappingForType(AnnotatedPerson.class); + assertThat(mapping.getPath(), is("bar")); + assertThat(mapping.getRel(), is("foo")); + assertThat(mapping.getSingleResourceRel(), is("foo.foo")); + assertThat(mapping.isExported(), is(false)); + } + + @Test + public void honorsAnnotatedsMapping() { + + CollectionResourceMapping mapping = factory.getMappingForType(PersonRepository.class); + assertThat(mapping.getPath(), is("bar")); + assertThat(mapping.getRel(), is("foo")); + assertThat(mapping.getSingleResourceRel(), is("foo.foo")); + assertThat(mapping.isExported(), is(false)); + } + + @Test + public void repositoryAnnotationTrumpsDomainTypeMapping() { + + CollectionResourceMapping mapping = factory.getMappingForType(AnnotatedAnnotatedPersonRepository.class); + assertThat(mapping.getPath(), is("trumpsAll")); + assertThat(mapping.getRel(), is("foo")); + assertThat(mapping.getSingleResourceRel(), is("foo.foo")); + assertThat(mapping.isExported(), is(true)); + } + + static class Person { + + } + + @RestResource(path = "bar", rel = "foo", exported = false) + static class AnnotatedPerson { + + } + + interface PersonRepository extends Repository { + + } + + @RestResource(path = "trumpsAll") + interface AnnotatedAnnotatedPersonRepository extends Repository { + + } +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/ResourceMappingsIntegrationTest.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/ResourceMappingsIntegrationTest.java new file mode 100644 index 000000000..1c555aa6b --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/mapping/ResourceMappingsIntegrationTest.java @@ -0,0 +1,60 @@ +/* + * 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 static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.repository.domain.jpa.JpaRepositoryConfig; +import org.springframework.data.rest.repository.domain.jpa.Person; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration tests for {@link ResourceMappings}. + * + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = JpaRepositoryConfig.class) +public class ResourceMappingsIntegrationTest { + + @Autowired ListableBeanFactory factory; + + ResourceMappings mappings; + + @Before + public void setUp() { + + Repositories repositories = new Repositories(factory); + this.mappings = new ResourceMappings(new RepositoryRestConfiguration(), repositories); + } + + @Test + public void foo() { + + assertThat(mappings, is(Matchers. iterableWithSize(2))); + assertThat(mappings.getMappingFor(Person.class).isExported(), is(false)); + } +} diff --git a/spring-data-rest-repository/src/test/resources/logback.xml b/spring-data-rest-repository/src/test/resources/logback.xml new file mode 100644 index 000000000..ad5cbef50 --- /dev/null +++ b/spring-data-rest-repository/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + \ No newline at end of file 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 5bdee50f0..d531c994c 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 @@ -1,137 +1,119 @@ +/* + * Copyright 2012-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.webmvc; +import static org.springframework.data.rest.webmvc.ControllerUtils.*; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceAware; import org.springframework.core.convert.ConversionFailedException; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Page; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.model.BeanWrapper; -import org.springframework.data.repository.core.RepositoryInformation; -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.BaseUriAwareResource; -import org.springframework.data.rest.repository.PagingAndSorting; -import org.springframework.data.rest.repository.PersistentEntityResource; import org.springframework.data.rest.repository.RepositoryConstraintViolationException; -import org.springframework.data.rest.repository.invoke.MethodParameterConversionService; -import org.springframework.data.rest.repository.support.ResourceMappingUtils; -import org.springframework.data.rest.webmvc.support.BaseUriLinkBuilder; import org.springframework.data.rest.webmvc.support.ExceptionMessage; import org.springframework.data.rest.webmvc.support.RepositoryConstraintViolationExceptionMessage; import org.springframework.data.rest.webmvc.support.ValidationExceptionHandler; -import org.springframework.hateoas.*; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.util.UriComponentsBuilder; - -import javax.servlet.http.HttpServletRequest; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URI; -import java.util.*; - -import static org.springframework.data.rest.core.util.UriUtils.buildUri; /** * @author Jon Brisbin + * @author Oliver Gierke */ -@SuppressWarnings({"rawtypes"}) -public class AbstractRepositoryRestController implements ApplicationContextAware, - InitializingBean { +@SuppressWarnings({ "rawtypes", "deprecation" }) +class AbstractRepositoryRestController implements MessageSourceAware, InitializingBean { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractRepositoryRestController.class); - static final Resource EMPTY_RESOURCE = new Resource(Collections.emptyList()); - static final Resources> EMPTY_RESOURCES = new Resources>(Collections.>emptyList()); - static final Iterable> EMPTY_RESOURCE_LIST = Collections.emptyList(); - static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); - protected final Logger LOG = LoggerFactory.getLogger(getClass()); - protected final Repositories repositories; - protected final RepositoryRestConfiguration config; - protected final DomainClassConverter domainClassConverter; - protected final ConversionService conversionService; - protected final MethodParameterConversionService methodParameterConversionService; - protected final EntityLinks entityLinks; - protected ApplicationContext applicationContext; - @Autowired(required = false) - protected ValidationExceptionHandler handler; - @Autowired(required = false) - protected PlatformTransactionManager txMgr; - protected TransactionTemplate txTmpl; + private final PersistentEntityResourceAssembler perAssembler; - @Autowired - public AbstractRepositoryRestController(Repositories repositories, - RepositoryRestConfiguration config, - DomainClassConverter domainClassConverter, - ConversionService conversionService, - EntityLinks entityLinks) { - this.repositories = repositories; - this.config = config; - this.domainClassConverter = domainClassConverter; - this.conversionService = conversionService; - this.entityLinks = entityLinks; - this.methodParameterConversionService = new MethodParameterConversionService(conversionService); + @Autowired(required = false) private ValidationExceptionHandler handler; + @Autowired(required = false) private PlatformTransactionManager txMgr; + + private MessageSource messageSource; + private PagedResourcesAssembler assembler; + + public AbstractRepositoryRestController(PagedResourcesAssembler assembler, PersistentEntityResourceAssembler entityResourceAssembler) { + + this.assembler = assembler; + this.perAssembler = entityResourceAssembler; } + /* + * (non-Javadoc) + * @see org.springframework.context.MessageSourceAware#setMessageSource(org.springframework.context.MessageSource) + */ @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; + public void setMessageSource(MessageSource messageSource) { + this.messageSource = messageSource; } @Override public void afterPropertiesSet() throws Exception { - if (null != txMgr) { - txTmpl = new TransactionTemplate(txMgr); - txTmpl.afterPropertiesSet(); - } + + // FIXME: + +// if (null != txMgr) { +// txTmpl = new TransactionTemplate(txMgr); +// txTmpl.afterPropertiesSet(); +// } } - @ExceptionHandler({ - NullPointerException.class - }) + @ExceptionHandler({ NullPointerException.class }) @ResponseBody public ResponseEntity handleNPE(NullPointerException npe) { return errorResponse(npe, HttpStatus.INTERNAL_SERVER_ERROR); } - @ExceptionHandler({ - ResourceNotFoundException.class - }) + @ExceptionHandler({ ResourceNotFoundException.class }) @ResponseBody public ResponseEntity handleNotFound() { return notFound(); } - @ExceptionHandler({ - NoSuchMethodError.class, - HttpRequestMethodNotSupportedException.class - }) + @ExceptionHandler({ NoSuchMethodError.class, HttpRequestMethodNotSupportedException.class }) @ResponseBody public ResponseEntity handleNoSuchMethod() { return errorResponse(null, HttpStatus.METHOD_NOT_ALLOWED); } - @ExceptionHandler({ - HttpMessageNotReadableException.class, - HttpMessageNotWritableException.class - }) + @ExceptionHandler({ HttpMessageNotReadableException.class, HttpMessageNotWritableException.class }) @ResponseBody public ResponseEntity handleNotReadable(HttpMessageNotReadableException e) { return badRequest(e); @@ -139,16 +121,12 @@ public class AbstractRepositoryRestController implements ApplicationContextAware /** * Handle failures commonly thrown from code tries to read incoming data and convert or cast it to the right type. - * + * * @param t * @return */ - @ExceptionHandler({ - InvocationTargetException.class, - IllegalArgumentException.class, - ClassCastException.class, - ConversionFailedException.class - }) + @ExceptionHandler({ InvocationTargetException.class, IllegalArgumentException.class, ClassCastException.class, + ConversionFailedException.class }) @ResponseBody public ResponseEntity handleMiscFailures(Throwable t) { if (null != t.getCause() && t.getCause() instanceof ResourceNotFoundException) { @@ -157,48 +135,22 @@ public class AbstractRepositoryRestController implements ApplicationContextAware return badRequest(t); } - // @ExceptionHandler({ - // RuntimeException.class - // }) - // @ResponseBody - // public ResponseEntity maybeHandleValidationException(Locale locale, - // RuntimeException ex) { - // if(ResourceNotFoundException.class.isAssignableFrom(ex.getClass())) { - // return handleNotFound(); - // } - // - // if(null != handler) { - // return handler.handleValidationException(ex, - // applicationContext, - // locale); - // } else { - // return response(null, - // ex, - // HttpStatus.BAD_REQUEST); - // } - // } - - @ExceptionHandler({ - RepositoryConstraintViolationException.class - }) + @ExceptionHandler({ RepositoryConstraintViolationException.class }) @ResponseBody public ResponseEntity handleRepositoryConstraintViolationException(Locale locale, - RepositoryConstraintViolationException rcve) { - return response(null, - new RepositoryConstraintViolationExceptionMessage(rcve, applicationContext, locale), - HttpStatus.BAD_REQUEST); + RepositoryConstraintViolationException rcve) { + + return response(null, new RepositoryConstraintViolationExceptionMessage(rcve, messageSource, locale), + HttpStatus.BAD_REQUEST); } /** * Send a 409 Conflict in case of concurrent modification. - * + * * @param ex * @return */ - @ExceptionHandler({ - OptimisticLockingFailureException.class, - DataIntegrityViolationException.class - }) + @ExceptionHandler({ OptimisticLockingFailureException.class, DataIntegrityViolationException.class }) @ResponseBody public ResponseEntity handleConflict(Exception ex) { return errorResponse(null, ex, HttpStatus.CONFLICT); @@ -220,14 +172,12 @@ public class AbstractRepositoryRestController implements ApplicationContextAware return errorResponse(headers, throwable, HttpStatus.BAD_REQUEST); } - public ResponseEntity errorResponse(T throwable, - HttpStatus status) { + public ResponseEntity errorResponse(T throwable, HttpStatus status) { return errorResponse(null, throwable, status); } - public ResponseEntity errorResponse(HttpHeaders headers, - T throwable, - HttpStatus status) { + public ResponseEntity errorResponse(HttpHeaders headers, T throwable, + HttpStatus status) { if (null != throwable && null != throwable.getMessage()) { LOG.error(throwable.getMessage(), throwable); return response(headers, new ExceptionMessage(throwable), status); @@ -244,86 +194,6 @@ public class AbstractRepositoryRestController implements ApplicationContextAware return new ResponseEntity(body, hdrs, status); } - public > ResponseEntity> resourceResponse(HttpHeaders headers, - R resource, - HttpStatus status) { - HttpHeaders hdrs = new HttpHeaders(); - if (null != headers) { - hdrs.putAll(headers); - } - return new ResponseEntity>(resource, hdrs, status); - } - - protected void addQueryParameters(HttpServletRequest request, - UriComponentsBuilder builder) { - for (Enumeration names = request.getParameterNames(); names.hasMoreElements(); ) { - String name = names.nextElement(); - String value = request.getParameter(name); - if (name.equals(config.getPageParamName()) || name.equals(config.getLimitParamName())) { - continue; - } - - builder.queryParam(name, value); - } - } - - protected Link searchLink(RepositoryRestRequest repoRequest, - int pageIncrement, - String method, - String rel) { - PagingAndSorting pageSort = repoRequest.getPagingAndSorting(); - UriComponentsBuilder ucb = UriComponentsBuilder.fromUri( - entityLinks.linkFor(repoRequest.getPersistentEntity().getType()) - .slash("search") - .slash(method) - .toUri() - ); - ucb.queryParam(config.getPageParamName(), Math.max(pageSort.getPageNumber() + pageIncrement, 1)) - .queryParam(config.getLimitParamName(), pageSort.getPageSize()); - - addQueryParameters(repoRequest.getRequest(), ucb); - - return new Link(ucb.build().toString(), rel); - } - - protected Link entitiesPageLink(RepositoryRestRequest repoRequest, - int pageIncrement, - String rel) { - PagingAndSorting pageSort = repoRequest.getPagingAndSorting(); - UriComponentsBuilder ucb = UriComponentsBuilder.fromUri( - entityLinks.linkFor(repoRequest.getPersistentEntity().getType()) - .toUri() - ); - if (null != repoRequest.getRequest().getParameter(config.getPageParamName())) { - ucb.queryParam(config.getPageParamName(), Math.max(pageSort.getPageNumber() + pageIncrement, 1)) - .queryParam(config.getLimitParamName(), pageSort.getPageSize()); - } - - addQueryParameters(repoRequest.getRequest(), ucb); - - return new Link(ucb.build().toString(), rel); - } - - 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()) { - continue; - } - links.add(linkBuilder.slash(methodMapping.getPath()) - .withRel(repoMapping.getRel() + "." + methodMapping.getRel())); - } - return links; - } - protected Link resourceLink(RepositoryRestRequest repoRequest, Resource resource) { ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping(); ResourceMapping entityMapping = repoRequest.getPersistentEntityResourceMapping(); @@ -333,89 +203,37 @@ public class AbstractRepositoryRestController implements ApplicationContextAware return new Link(selfLink.getHref(), rel); } - @SuppressWarnings({"unchecked"}) - protected Resources resultToResources(RepositoryRestRequest repoRequest, - Object result, - List links, - Link prevLink, - Link nextLink) { + @SuppressWarnings({ "unchecked" }) + protected Resources resultToResources(Object result, Link baseLink) { + if (result instanceof Page) { - Page page = (Page) result; - PagedResources.PageMetadata pageMeta = pageMetadata(page); - if (page.hasPreviousPage() && null != prevLink) { - links.add(prevLink); - } - if (page.hasNextPage() && null != nextLink) { - links.add(nextLink); - } - if (page.hasContent()) { - return entitiesToResources(repoRequest, links, page); - } else { - return new PagedResources(Collections.emptyList(), pageMeta, links); - } + Page page = (Page) result; + return entitiesToResources(page, baseLink, assembler); } else if (result instanceof Iterable) { - return entitiesToResources(repoRequest, links, (Iterable) result); + return entitiesToResources((Iterable) result); } else if (null == result) { return new Resources(EMPTY_RESOURCE_LIST); } else { - PersistentEntityResource per = PersistentEntityResource.wrap( - repoRequest.getPersistentEntity(), - result, - repoRequest.getBaseUri() - ); - BeanWrapper wrapper = BeanWrapper.create(result, conversionService); - Link selfLink = entityLinks.linkForSingleResource( - result.getClass(), - wrapper.getProperty(repoRequest.getPersistentEntity().getIdProperty()) - ) - .withSelfRel(); - per.add(selfLink); - return new Resources(Collections.singletonList(per), links); + Resource resource = perAssembler.toResource(result); + return new Resources(Collections.singletonList(resource)); } } - @SuppressWarnings({"unchecked"}) - protected Resources entitiesToResources(RepositoryRestRequest repoRequest, List links, Page page) { - PagedResources.PageMetadata pageMeta = pageMetadata(page); - Resources resource = (Resources) entitiesToResources(repoRequest, links, page.getContent()); - return new PagedResources(resource.getContent(), pageMeta, resource.getLinks()); + protected Resources> entitiesToResources(Page page, Link baseLink, + final PagedResourcesAssembler assembler) { + + return assembler.toResource(page, perAssembler, baseLink); } - @SuppressWarnings({"unchecked"}) - protected Resources entitiesToResources(RepositoryRestRequest repoRequest, List links, Iterable entities) { - List> resources = new ArrayList>(); + + protected Resources> entitiesToResources(Iterable entities) { + + List> resources = new ArrayList>(); + for (Object obj : entities) { - if (null == obj) { - resources.add(null); - continue; - } - - PersistentEntity persistentEntity = repositories.getPersistentEntity(obj.getClass()); - if (null == persistentEntity) { - resources.add(new BaseUriAwareResource(obj) - .setBaseUri(repoRequest.getBaseUri())); - continue; - } - - BeanWrapper wrapper = BeanWrapper.create(obj, conversionService); - PersistentEntityResource per = PersistentEntityResource.wrap(persistentEntity, obj, repoRequest.getBaseUri()); - Link selfLink = entityLinks.linkForSingleResource(persistentEntity.getType(), - wrapper.getProperty(persistentEntity.getIdProperty())) - .withSelfRel(); - per.add(selfLink); - resources.add(per); + resources.add(obj == null ? null : perAssembler.toResource(obj)); } - return new Resources(resources, links); + return new Resources>(resources); } - - protected PagedResources.PageMetadata pageMetadata(Page page) { - return new PagedResources.PageMetadata( - page.getNumberOfElements(), - page.getNumber() + 1, - page.getTotalElements(), - page.getTotalPages() - ); - } - } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriMethodArgumentResolver.java index 0240895a0..c4a18d660 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/BaseUriMethodArgumentResolver.java @@ -1,9 +1,23 @@ +/* + * Copyright 2012-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.webmvc; import java.net.URI; import javax.servlet.http.HttpServletRequest; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.data.rest.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.annotation.BaseURI; @@ -15,14 +29,11 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; /** * @author Jon Brisbin + * @author Oliver Gierke */ public class BaseUriMethodArgumentResolver implements HandlerMethodArgumentResolver { - @Autowired - private RepositoryRestConfiguration config; - - public BaseUriMethodArgumentResolver() { - } + private final RepositoryRestConfiguration config; public BaseUriMethodArgumentResolver(RepositoryRestConfiguration config) { this.config = config; @@ -34,7 +45,7 @@ public class BaseUriMethodArgumentResolver implements HandlerMethodArgumentResol } @Override - public Object resolveArgument(MethodParameter parameter, + public URI resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { @@ -48,5 +59,4 @@ public class BaseUriMethodArgumentResolver implements HandlerMethodArgumentResol return config.getBaseUri(); } - } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java new file mode 100644 index 000000000..fd6cd033f --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ControllerUtils.java @@ -0,0 +1,49 @@ +/* + * 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.webmvc; + +import java.util.Collections; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +/** + * + * @author Oliver Gierke + */ +public class ControllerUtils { + + public static final Resource EMPTY_RESOURCE = new Resource(Collections.emptyList()); + public static final Resources> EMPTY_RESOURCES = new Resources>( + Collections.> emptyList()); + public static final Iterable> EMPTY_RESOURCE_LIST = Collections.emptyList(); + public static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); + + public static > ResponseEntity> toResponseEntity(HttpHeaders headers, R resource, + HttpStatus status) { + + HttpHeaders hdrs = new HttpHeaders(); + if (null != headers) { + hdrs.putAll(headers); + } + + return new ResponseEntity>(resource, hdrs, status); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java deleted file mode 100644 index 36e3e5545..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; -import javax.servlet.http.HttpServletRequest; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.MethodParameter; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.rest.config.RepositoryRestConfiguration; -import org.springframework.data.rest.config.ResourceMapping; -import org.springframework.data.rest.repository.PagingAndSorting; -import org.springframework.data.web.PageableDefaults; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; - -/** - * {@link HandlerMethodArgumentResolver} implementation responsible for inspecting a request for page and sort - * parameters for use by the repositories. - * - * @author Jon Brisbin - */ -@SuppressWarnings("deprecation") -public class PagingAndSortingMethodArgumentResolver implements HandlerMethodArgumentResolver { - - private static final int DEFAULT_PAGE = 1; // We're 1-based, not 0-based - @Autowired - private RepositoryRestConfiguration config; - @Autowired - private RepositoryInformationHandlerMethodArgumentResolver repoInfoResolver; - - public PagingAndSortingMethodArgumentResolver() { - } - - public PagingAndSortingMethodArgumentResolver(RepositoryRestConfiguration config) { - this.config = config; - } - - public RepositoryRestConfiguration getConfig() { - return config; - } - - public RepositoryInformationHandlerMethodArgumentResolver getRepoInfoResolver() { - return repoInfoResolver; - } - - public PagingAndSortingMethodArgumentResolver setRepoInfoResolver(RepositoryInformationHandlerMethodArgumentResolver repoInfoResolver) { - this.repoInfoResolver = repoInfoResolver; - return this; - } - - @Override public boolean supportsParameter(MethodParameter parameter) { - return ClassUtils.isAssignable(parameter.getParameterType(), PagingAndSorting.class); - } - - @Override - public Object resolveArgument(MethodParameter parameter, - ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - WebDataBinderFactory binderFactory) throws Exception { - HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(); - - PageRequest pr = null; - for(Annotation annotation : parameter.getParameterAnnotations()) { - if(annotation instanceof PageableDefaults) { - PageableDefaults defaults = (PageableDefaults)annotation; - pr = new PageRequest(defaults.pageNumber(), defaults.value()); - break; - } - } - if(null == pr) { - int page = DEFAULT_PAGE; - String sPage = request.getParameter(config.getPageParamName()); - if(StringUtils.hasText(sPage)) { - try { - page = Integer.parseInt(sPage); - } catch(NumberFormatException ignored) { - } - } - int limit = config.getDefaultPageSize(); - String sLimit = request.getParameter(config.getLimitParamName()); - if(StringUtils.hasText(sLimit)) { - try { - limit = Math.min(Integer.parseInt(sLimit), config.getMaxPageSize()); - } catch(NumberFormatException ignored) { - } - } - - Sort sort = null; - List orders = new ArrayList(); - String[] orderValues = request.getParameterValues(config.getSortParamName()); - if(null != orderValues) { - for(String orderParam : orderValues) { - String name = nameForParam(parameter, - mavContainer, - webRequest, - binderFactory, - orderParam); - String sortDir = request.getParameter(orderParam + ".dir"); - Sort.Direction dir = (null != sortDir ? Sort.Direction.valueOf(sortDir.toUpperCase()) : Sort.Direction.ASC); - orders.add(new Sort.Order(dir, name)); - } - if(!orders.isEmpty()) { - sort = new Sort(orders); - } - } - - if(null != sort) { - pr = new PageRequest(page - 1, limit, sort); - } else { - pr = new PageRequest(page - 1, limit); - } - } - - return new PagingAndSorting(config, pr); - } - - private String nameForParam(MethodParameter parameter, - ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - WebDataBinderFactory binderFactory, - String orderParam) throws Exception { - if(null == repoInfoResolver) { - return orderParam; - } - RepositoryInformation repoInfo = (RepositoryInformation)repoInfoResolver.resolveArgument(parameter, - mavContainer, - webRequest, - binderFactory); - ResourceMapping repoMapping = config.getResourceMappingForDomainType(repoInfo.getDomainType()); - if(null != repoMapping) { - return repoMapping.getNameForPath(orderParam); - } - return orderParam; - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java new file mode 100644 index 000000000..993562425 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceAssembler.java @@ -0,0 +1,74 @@ +/* + * 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.webmvc; + +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.BeanWrapper; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.repository.PersistentEntityResource; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.ResourceAssembler; +import org.springframework.util.Assert; + +/** + * + * @author Oliver Gierke + */ +public class PersistentEntityResourceAssembler implements ResourceAssembler> { + + private final Repositories repositories; + private final EntityLinks entityLinks; + + /** + * Creates a new {@link PersistentEntityResourceAssembler}. + * + * @param repositories must not be {@literal null}. + * @param entityLinks must not be {@literal null}. + */ + public PersistentEntityResourceAssembler(Repositories repositories, EntityLinks entityLinks) { + + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(entityLinks, "EntityLinks must not be null!"); + + this.repositories = repositories; + this.entityLinks = entityLinks; + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.ResourceAssembler#toResource(java.lang.Object) + */ + @Override + public PersistentEntityResource toResource(T instance) { + + PersistentEntity entity = repositories.getPersistentEntity(instance.getClass()); + + PersistentEntityResource resource = PersistentEntityResource.wrap(entity, instance); + resource.add(getSelfLinkFor(instance)); + return resource; + } + + public Link getSelfLinkFor(Object instance) { + + PersistentEntity entity = repositories.getPersistentEntity(instance.getClass()); + + BeanWrapper wrapper = BeanWrapper.create(instance, null); + Object id = wrapper.getProperty(entity.getIdProperty()); + + return entityLinks.linkForSingleResource(entity.getType(), id).withSelfRel(); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java index db9a5357b..c43d6da8b 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PersistentEntityResourceHandlerMethodArgumentResolver.java @@ -49,8 +49,7 @@ public class PersistentEntityResourceHandlerMethodArgumentResolver implements Ha } Object obj = converter.read(domainType, request); - return new PersistentEntityResource(repoRequest.getPersistentEntity(), - obj); + return new PersistentEntityResource(repoRequest.getPersistentEntity(), obj); } return null; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java index 1dea9cabd..d03584d16 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryController.java @@ -1,11 +1,25 @@ +/* + * Copyright 2012-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.webmvc; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.convert.ConversionService; -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.web.PagedResourcesAssembler; import org.springframework.hateoas.EntityLinks; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -15,30 +29,29 @@ import static org.springframework.data.rest.repository.support.ResourceMappingUt /** * @author Jon Brisbin + * @author Oliver Gierke */ +@RestController +@SuppressWarnings("deprecation") public class RepositoryController extends AbstractRepositoryRestController { + private final Repositories repositories; + private final RepositoryRestConfiguration config; + private final EntityLinks entityLinks; + @Autowired - public RepositoryController(Repositories repositories, - RepositoryRestConfiguration config, - DomainClassConverter domainClassConverter, - ConversionService conversionService, - EntityLinks entityLinks) { - super(repositories, - config, - domainClassConverter, - conversionService, - entityLinks); + public RepositoryController(Repositories repositories, RepositoryRestConfiguration config, EntityLinks entityLinks, + PagedResourcesAssembler assembler, PersistentEntityResourceAssembler perAssembler) { + + super(assembler, perAssembler); + + this.repositories = repositories; + this.config = config; + this.entityLinks = entityLinks; } - @RequestMapping( - value = "/", - method = RequestMethod.GET, - produces = { - "application/json", - "application/x-spring-data-compact+json" - } - ) + @RequestMapping(value = "/", method = RequestMethod.GET, // + produces = { "application/json", "application/x-spring-data-compact+json" }) @ResponseBody public RepositoryLinksResource listRepositories() throws ResourceNotFoundException { RepositoryLinksResource resource = new RepositoryLinksResource(); @@ -50,5 +63,4 @@ public class RepositoryController extends AbstractRepositoryRestController { } return resource; } - } 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 529bbae44..009ed92ec 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 @@ -1,34 +1,21 @@ +/* + * 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.webmvc; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.mapping.PersistentProperty; -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.PagingAndSorting; -import org.springframework.data.rest.repository.PersistentEntityResource; -import org.springframework.data.rest.repository.context.*; -import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; -import org.springframework.data.rest.repository.json.JsonSchema; -import org.springframework.data.rest.repository.json.PersistentEntityToJsonSchemaConverter; -import org.springframework.data.rest.repository.support.DomainObjectMerger; -import org.springframework.hateoas.*; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.web.HttpRequestMethodNotSupportedException; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; +import static org.springframework.data.rest.webmvc.ControllerUtils.*; import java.io.Serializable; import java.net.URI; @@ -36,109 +23,147 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mapping.PersistentProperty; +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; +import org.springframework.data.rest.repository.context.AfterSaveEvent; +import org.springframework.data.rest.repository.context.BeforeCreateEvent; +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.support.DomainObjectMerger; +import org.springframework.data.rest.webmvc.json.JsonSchema; +import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.PagedResources; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionOperations; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + /** * @author Jon Brisbin + * @author Oliver Gierke */ -@SuppressWarnings({"rawtypes"}) -public class RepositoryEntityController extends AbstractRepositoryRestController { +@RestController +@SuppressWarnings("deprecation") +class RepositoryEntityController extends AbstractRepositoryRestController implements + ApplicationEventPublisherAware { private static final String BASE_MAPPING = "/{repository}"; - - @Autowired - private DomainObjectMerger domainObjectMerger; - @Autowired - private PersistentEntityToJsonSchemaConverter jsonSchemaConverter; - public RepositoryEntityController(Repositories repositories, - RepositoryRestConfiguration config, - DomainClassConverter domainClassConverter, - ConversionService conversionService, - EntityLinks entityLinks) { - super(repositories, - config, - domainClassConverter, - conversionService, - entityLinks); + private final EntityLinks entityLinks; + private final PersistentEntityResourceAssembler perAssembler; + private final RepositoryRestConfiguration config; + private final DomainClassConverter converter; + private final ConversionService conversionService; + + private final TransactionOperations txOperations; + + private ApplicationEventPublisher publisher; + @Autowired private DomainObjectMerger domainObjectMerger; + @Autowired private PersistentEntityToJsonSchemaConverter jsonSchemaConverter; + + @Autowired + public RepositoryEntityController(Repositories repositories, RepositoryRestConfiguration config, + EntityLinks entityLinks, PagedResourcesAssembler assembler, + PersistentEntityResourceAssembler perAssembler, DomainClassConverter converter, + @Qualifier("defaultConversionService") ConversionService conversionService) { + + super(assembler, perAssembler); + + this.entityLinks = entityLinks; + this.perAssembler = perAssembler; + this.config = config; + this.converter = converter; + this.conversionService = conversionService; + + this.txOperations = null; } - @RequestMapping( - value = BASE_MAPPING + "/schema", - method = RequestMethod.GET, - produces = { - "application/schema+json" - } - ) + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) + */ + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + + } + + @RequestMapping(value = BASE_MAPPING + "/schema", method = RequestMethod.GET, + produces = { "application/schema+json" }) @ResponseBody public JsonSchema schema(RepositoryRestRequest repoRequest) { return jsonSchemaConverter.convert(repoRequest.getPersistentEntity().getType()); } - @RequestMapping( - value = BASE_MAPPING, - method = RequestMethod.GET, - produces = { - "application/json", - "application/x-spring-data-verbose+json" - } - ) + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/json", + "application/x-spring-data-verbose+json" }) @ResponseBody - public Resources listEntities(final RepositoryRestRequest repoRequest) + public Resources listEntities(final RepositoryRestRequest request, Pageable pageable) throws ResourceNotFoundException { List links = new ArrayList(); Iterable results; - RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + RepositoryMethodInvoker repoMethodInvoker = request.getRepositoryMethodInvoker(); if (null == repoMethodInvoker) { throw new ResourceNotFoundException(); } - boolean hasSortParams = (null != repoRequest.getRequest().getParameter(config.getSortParamName())); + if (repoMethodInvoker.hasFindAllPageable()) { - PagingAndSorting pageSort = repoRequest.getPagingAndSorting(); - results = repoMethodInvoker.findAll(new PageRequest(pageSort.getPageNumber(), - pageSort.getPageSize(), - pageSort.getSort())); - } else if (repoMethodInvoker.hasFindAllSorted() && hasSortParams) { - results = repoMethodInvoker.findAll(repoRequest.getPagingAndSorting().getSort()); + results = repoMethodInvoker.findAll(pageable); + } else if (repoMethodInvoker.hasFindAllSorted()) { + results = repoMethodInvoker.findAll(pageable.getSort()); } else if (repoMethodInvoker.hasFindAll()) { results = repoMethodInvoker.findAll(); } else { throw new ResourceNotFoundException(); } - ResourceMapping repoMapping = repoRequest.getRepositoryResourceMapping(); + ResourceMapping repoMapping = request.getRepositoryResourceMapping(); if (!repoMethodInvoker.getQueryMethods().isEmpty()) { - links.add(entityLinks.linkForSingleResource(repoRequest.getPersistentEntity().getType(), "search") - .withRel(repoMapping.getRel() + ".search")); + links.add(entityLinks.linkForSingleResource(request.getPersistentEntity().getType(), "search").withRel( + repoMapping.getRel() + ".search")); } - PagingAndSorting pageSort = repoRequest.getPagingAndSorting(); - Link prevLink = null; - Link nextLink = null; - if (results instanceof Page) { - if (((Page) results).hasPreviousPage() && pageSort.getPageNumber() > 0) { - prevLink = entitiesPageLink(repoRequest, 0, "page.previous"); - } - if (((Page) results).hasNextPage()) { - nextLink = entitiesPageLink(repoRequest, 1, "page.next"); - } - } - - return resultToResources(repoRequest, results, links, prevLink, nextLink); + Link baseLink = request.getRepositoryLink(); + Resources resources = resultToResources(results, baseLink); + resources.add(links); + return resources; } - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = BASE_MAPPING, - method = RequestMethod.GET, - produces = { - "application/x-spring-data-compact+json", - "text/uri-list" - } - ) + @SuppressWarnings({ "unchecked" }) + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { + "application/x-spring-data-compact+json", "text/uri-list" }) @ResponseBody - public Resources listEntitiesCompact(final RepositoryRestRequest repoRequest) + public Resources listEntitiesCompact(final RepositoryRestRequest repoRequest, Pageable pageable) throws ResourceNotFoundException { - Resources resources = listEntities(repoRequest); + Resources resources = listEntities(repoRequest, pageable); List links = new ArrayList(resources.getLinks()); for (Resource resource : ((Resources>) resources).getContent()) { @@ -146,169 +171,115 @@ public class RepositoryEntityController extends AbstractRepositoryRestController links.add(resourceLink(repoRequest, persistentEntityResource)); } if (resources instanceof PagedResources) { - return new PagedResources(Collections.emptyList(), ((PagedResources) resources).getMetadata(), links); + return new PagedResources(Collections.emptyList(), ((PagedResources) resources).getMetadata(), links); } else { - return new Resources(Collections.emptyList(), links); + return new Resources(Collections.emptyList(), links); } } - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = BASE_MAPPING, - method = RequestMethod.POST, - consumes = { - "application/json" - }, - produces = { - "application/json", - "text/uri-list" - } - ) + @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST, consumes = { "application/json" }, produces = { + "application/json", "text/uri-list" }) @ResponseBody public ResponseEntity> createNewEntity(RepositoryRestRequest repoRequest, - PersistentEntityResource incoming) { + PersistentEntityResource incoming) { RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); if (null == repoMethodInvoker || !repoMethodInvoker.hasSaveOne()) { throw new NoSuchMethodError(); } - applicationContext.publishEvent(new BeforeCreateEvent(incoming.getContent())); + publisher.publishEvent(new BeforeCreateEvent(incoming.getContent())); Object obj = repoMethodInvoker.save(incoming.getContent()); - applicationContext.publishEvent(new AfterCreateEvent(obj)); + publisher.publishEvent(new AfterCreateEvent(obj)); - BeanWrapper wrapper = BeanWrapper.create(obj, conversionService); - Link selfLink = entityLinks.linkForSingleResource( - repoRequest.getPersistentEntity().getType(), - wrapper.getProperty(repoRequest.getPersistentEntity().getIdProperty()) - ).withSelfRel(); + Link selfLink = perAssembler.getSelfLinkFor(obj); HttpHeaders headers = new HttpHeaders(); headers.setLocation(URI.create(selfLink.getHref())); if (config.isReturnBodyOnCreate()) { - return resourceResponse( - headers, - new PersistentEntityResource( - repoRequest.getPersistentEntity(), - obj, - selfLink - ).setBaseUri(repoRequest.getBaseUri()), - HttpStatus.CREATED - ); + return ControllerUtils.toResponseEntity(headers, perAssembler.toResource(obj), HttpStatus.CREATED); } else { - return resourceResponse(headers, null, HttpStatus.CREATED); + return ControllerUtils.toResponseEntity(headers, null, HttpStatus.CREATED); } } - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = BASE_MAPPING + "/{id}", - method = RequestMethod.GET, - produces = { - "application/json", - "application/x-spring-data-compact+json", - "text/uri-list" - } - ) + /** + * {@code GET /{repository}/{id}} + * + * @param repoRequest + * @param id + * @return + * @throws ResourceNotFoundException + */ + @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET, produces = { "application/json", + "application/x-spring-data-compact+json", "text/uri-list" }) @ResponseBody - public Resource getSingleEntity(RepositoryRestRequest repoRequest, - @PathVariable String id) throws ResourceNotFoundException { + public Resource getSingleEntity(RepositoryRestRequest repoRequest, @PathVariable String id) + throws ResourceNotFoundException { RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); if (null == repoMethodInvoker || !repoMethodInvoker.hasFindOne()) { throw new ResourceNotFoundException(); } - Object domainObj = domainClassConverter.convert( - id, - STRING_TYPE, - TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType()) - ); + Object domainObj = converter.convert(id, STRING_TYPE, + TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType())); + if (null == domainObj) { throw new ResourceNotFoundException(); } - PersistentEntityResource per = PersistentEntityResource.wrap(repoRequest.getPersistentEntity(), - domainObj, - repoRequest.getBaseUri()); - BeanWrapper wrapper = BeanWrapper.create(domainObj, conversionService); - Link selfLink = entityLinks.linkForSingleResource( - repoRequest.getPersistentEntity().getType(), - wrapper.getProperty(repoRequest.getPersistentEntity().getIdProperty()) - ).withSelfRel(); - per.add(selfLink); - return per; + return perAssembler.toResource(domainObj); } - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = BASE_MAPPING + "/{id}", - method = RequestMethod.PUT, - consumes = { - "application/json" - }, - produces = { - "application/json", - "text/uri-list" - } - ) + /** + * {@code PUT /{repository}/{id}} - Updates an existing entity or creates one at exactly that place. + * + * @param repoRequest + * @param incoming + * @param id + * @return + * @throws ResourceNotFoundException + */ + @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT, consumes = { "application/json" }, + produces = { "application/json", "text/uri-list" }) @ResponseBody public ResponseEntity> updateEntity(RepositoryRestRequest repoRequest, - PersistentEntityResource incoming, - @PathVariable String id) throws ResourceNotFoundException { + PersistentEntityResource incoming, @PathVariable String id) throws ResourceNotFoundException { + RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); if (null == repoMethodInvoker || !repoMethodInvoker.hasSaveOne() || !repoMethodInvoker.hasFindOne()) { throw new NoSuchMethodError(); } - Object domainObj = domainClassConverter.convert( - id, - STRING_TYPE, - TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType()) - ); + Object domainObj = converter.convert(id, STRING_TYPE, + TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType())); if (null == domainObj) { - BeanWrapper incomingWrapper = BeanWrapper.create(incoming.getContent(), conversionService); - PersistentProperty idProp = incoming.getPersistentEntity().getIdProperty(); + BeanWrapper incomingWrapper = BeanWrapper.create(incoming.getContent(), conversionService); + PersistentProperty idProp = incoming.getPersistentEntity().getIdProperty(); incomingWrapper.setProperty(idProp, conversionService.convert(id, idProp.getType())); return createNewEntity(repoRequest, incoming); } domainObjectMerger.merge(incoming.getContent(), domainObj); - applicationContext.publishEvent(new BeforeSaveEvent(incoming.getContent())); + publisher.publishEvent(new BeforeSaveEvent(incoming.getContent())); Object obj = repoMethodInvoker.save(domainObj); - applicationContext.publishEvent(new AfterSaveEvent(obj)); + publisher.publishEvent(new AfterSaveEvent(obj)); if (config.isReturnBodyOnUpdate()) { - PersistentEntityResource per = PersistentEntityResource.wrap(repoRequest.getPersistentEntity(), - obj, - repoRequest.getBaseUri()); - BeanWrapper wrapper = BeanWrapper.create(obj, conversionService); - Link selfLink = entityLinks.linkForSingleResource(repoRequest.getPersistentEntity().getType(), - wrapper.getProperty(repoRequest.getPersistentEntity() - .getIdProperty())) - .withSelfRel(); - per.add(selfLink); - return resourceResponse(null, - per, - HttpStatus.OK); + return ControllerUtils.toResponseEntity(null, perAssembler.toResource(obj), HttpStatus.OK); } else { - return resourceResponse(null, - null, - HttpStatus.NO_CONTENT); + return ControllerUtils.toResponseEntity(null, null, HttpStatus.NO_CONTENT); } } - @SuppressWarnings({"unchecked"}) - @RequestMapping( - value = BASE_MAPPING + "/{id}", - method = RequestMethod.DELETE - ) + @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.DELETE) @ResponseBody - public ResponseEntity deleteEntity(final RepositoryRestRequest repoRequest, - @PathVariable final String id) + public ResponseEntity deleteEntity(final RepositoryRestRequest repoRequest, @PathVariable final String id) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (null == repoMethodInvoker || (!repoMethodInvoker.hasFindOne() - && !(repoMethodInvoker.hasDeleteOne() || repoMethodInvoker.hasDeleteOneById()))) { + if (null == repoMethodInvoker + || (!repoMethodInvoker.hasFindOne() && !(repoMethodInvoker.hasDeleteOne() || repoMethodInvoker + .hasDeleteOneById()))) { throw new HttpRequestMethodNotSupportedException("DELETE"); } ResourceMapping methodMapping = repoRequest.getRepositoryResourceMapping().getResourceMappingFor("delete"); @@ -316,22 +287,20 @@ public class RepositoryEntityController extends AbstractRepositoryRestController throw new HttpRequestMethodNotSupportedException("DELETE"); } - final Object domainObj = domainClassConverter.convert(id, - STRING_TYPE, - TypeDescriptor.valueOf(repoRequest.getPersistentEntity() - .getType())); + final Object domainObj = converter.convert(id, STRING_TYPE, + TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType())); if (null == domainObj) { throw new ResourceNotFoundException(); } - applicationContext.publishEvent(new BeforeDeleteEvent(domainObj)); + publisher.publishEvent(new BeforeDeleteEvent(domainObj)); TransactionCallbackWithoutResult callback = new TransactionCallbackWithoutResult() { @Override + @SuppressWarnings({ "unchecked" }) protected void doInTransactionWithoutResult(TransactionStatus status) { if (repoMethodInvoker.hasDeleteOneById()) { Class idType = (Class) repoRequest.getPersistentEntity() - .getIdProperty() - .getType(); + .getIdProperty().getType(); final Serializable idVal = conversionService.convert(id, idType); repoMethodInvoker.delete(idVal); } else if (repoMethodInvoker.hasDeleteOne()) { @@ -339,12 +308,16 @@ public class RepositoryEntityController extends AbstractRepositoryRestController } } }; - if (null != txTmpl) { - txTmpl.execute(callback); + + // FIXME + + if (txOperations != null) { + txOperations.execute(callback); } else { callback.doInTransaction(null); } - applicationContext.publishEvent(new AfterDeleteEvent(domainObj)); + + publisher.publishEvent(new AfterDeleteEvent(domainObj)); return new ResponseEntity(HttpStatus.NO_CONTENT); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryInformationHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryInformationHandlerMethodArgumentResolver.java index 75d43c763..a2790c05e 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryInformationHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryInformationHandlerMethodArgumentResolver.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.webmvc; import static org.springframework.util.ClassUtils.*; @@ -15,27 +30,31 @@ import org.springframework.web.util.UrlPathHelper; /** * @author Jon Brisbin + * @author Oliver Gierke */ -public class RepositoryInformationHandlerMethodArgumentResolver - extends RepositoryInformationSupport - implements HandlerMethodArgumentResolver { +public class RepositoryInformationHandlerMethodArgumentResolver extends RepositoryInformationSupport implements + HandlerMethodArgumentResolver { - @Override public boolean supportsParameter(MethodParameter parameter) { + @Override + public boolean supportsParameter(MethodParameter parameter) { return isAssignable(parameter.getParameterType(), RepositoryInformation.class); } @Override - public Object resolveArgument(MethodParameter parameter, + public RepositoryInformation resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); String requestUri = new UrlPathHelper().getLookupPathForRequest(request); + if(requestUri.startsWith("/")) { requestUri = requestUri.substring(1); } String[] parts = requestUri.split("/"); + if(parts.length == 0) { // Root request return null; @@ -43,6 +62,4 @@ public class RepositoryInformationHandlerMethodArgumentResolver return findRepositoryInfoFor(parts[0]); } - - } 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 e0de70d2b..47d3fa037 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 @@ -1,7 +1,23 @@ +/* + * Copyright 2012-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.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 java.net.URI; import java.util.ArrayList; @@ -10,7 +26,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.springframework.core.convert.ConversionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; @@ -26,7 +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.hateoas.EntityLinks; +import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.http.HttpHeaders; @@ -43,21 +61,39 @@ import org.springframework.web.bind.annotation.ResponseBody; * @author Jon Brisbin * @author Oliver Gierke */ -@SuppressWarnings({"unchecked", "rawtypes"}) -public class RepositoryPropertyReferenceController extends AbstractRepositoryRestController { +@RestController +@SuppressWarnings({"unchecked", "deprecation"}) +public class RepositoryPropertyReferenceController extends AbstractRepositoryRestController implements ApplicationEventPublisherAware { private static final String BASE_MAPPING = "/{repository}/{id}/{property}"; + + private final Repositories repositories; + private final RepositoryRestConfiguration config; + private final PersistentEntityResourceAssembler perAssembler; + private final DomainClassConverter converter; - public RepositoryPropertyReferenceController(Repositories repositories, - RepositoryRestConfiguration config, - DomainClassConverter domainClassConverter, - ConversionService conversionService, - EntityLinks entityLinks) { - super(repositories, - config, - domainClassConverter, - conversionService, - entityLinks); + private ApplicationEventPublisher publisher; + + @Autowired + public RepositoryPropertyReferenceController(Repositories repositories, RepositoryRestConfiguration config, + DomainClassConverter domainClassConverter, PagedResourcesAssembler assembler, + PersistentEntityResourceAssembler perAssembler) { + + super(assembler, perAssembler); + + this.repositories = repositories; + this.perAssembler = perAssembler; + this.config = config; + this.converter = domainClassConverter; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) + */ + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; } @RequestMapping( @@ -76,51 +112,36 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes final HttpHeaders headers = new HttpHeaders(); Function> handler = new Function>() { @Override public Resource apply(ReferencedProperty prop) { + if(null == prop.propertyValue) { throw new ResourceNotFoundException(); } + if(prop.property.isCollectionLike()) { + List> resources = new ArrayList>(); - PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType); - for(Object obj : ((Iterable)prop.propertyValue)) { - BeanWrapper wrapper = BeanWrapper.create(obj, conversionService); - PersistentEntityResource per = PersistentEntityResource.wrap(entity, obj, repoRequest.getBaseUri()); - Link selfLink = entityLinks.linkForSingleResource(entity.getType(), - wrapper.getProperty(entity.getIdProperty())) - .withSelfRel(); - per.add(selfLink); - resources.add(per); + + for(Object obj : ((Iterable) prop.propertyValue)) { + resources.add(perAssembler.toResource(obj)); } return new Resource(resources); + } else if(prop.property.isMap()) { + Map> resources = new HashMap>(); - PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType); - for(Map.Entry entry : ((Map)prop.propertyValue).entrySet()) { - PersistentEntityResource per = PersistentEntityResource.wrap(entity, - entry.getValue(), - repoRequest.getBaseUri()); - BeanWrapper wrapper = BeanWrapper.create(entry.getValue(), conversionService); - Link selfLink = entityLinks.linkForSingleResource(entity.getType(), - wrapper.getProperty(entity.getIdProperty())) - .withSelfRel(); - per.add(selfLink); - resources.put(entry.getKey(), per); + + for(Map.Entry entry : ((Map) prop.propertyValue).entrySet()) { + resources.put(entry.getKey(), perAssembler.toResource(entry.getValue())); } return new Resource(resources); + } else { - PersistentEntityResource per = PersistentEntityResource.wrap(repositories.getPersistentEntity(prop.propertyType), - prop.propertyValue, - repoRequest.getBaseUri()); - BeanWrapper wrapper = BeanWrapper.create(prop.propertyValue, conversionService); - Link selfLink = entityLinks.linkForSingleResource(prop.propertyType, - wrapper.getProperty(prop.entity.getIdProperty())) - .withSelfRel(); - per.add(selfLink); - headers.set("Content-Location", selfLink.getHref()); - - return new Resource(per); + + PersistentEntityResource resource = perAssembler.toResource(prop.propertyValue); + headers.set("Content-Location", resource.getId().getHref()); + return resource; } } }; @@ -128,7 +149,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes id, property, handler); - return resourceResponse(headers, responseResource, HttpStatus.OK); + return ControllerUtils.toResponseEntity(headers, responseResource, HttpStatus.OK); } @RequestMapping( @@ -158,9 +179,9 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes prop.wrapper.setProperty(prop.property, null); } - applicationContext.publishEvent(new BeforeLinkDeleteEvent(prop.wrapper.getBean(), prop.propertyValue)); + publisher.publishEvent(new BeforeLinkDeleteEvent(prop.wrapper.getBean(), prop.propertyValue)); Object result = repoMethodInvoker.save(prop.wrapper.getBean()); - applicationContext.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); + publisher.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); return null; } }; @@ -175,7 +196,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } } - return resourceResponse(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); + return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); } @RequestMapping( @@ -201,31 +222,29 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes throw new ResourceNotFoundException(); } if(prop.property.isCollectionLike()) { - PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType); - for(Object obj : ((Iterable)prop.propertyValue)) { - BeanWrapper propValWrapper = BeanWrapper.create(obj, conversionService); + for(Object obj : ((Iterable)prop.propertyValue)) { + + BeanWrapper propValWrapper = BeanWrapper.create(obj, null); String sId = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); + if(propertyId.equals(sId)) { - PersistentEntityResource per = PersistentEntityResource.wrap(entity, obj, repoRequest.getBaseUri()); - Link selfLink = entityLinks.linkForSingleResource(entity.getType(), sId).withSelfRel(); - per.add(selfLink); - headers.set("Content-Location", selfLink.getHref()); - return per; + + PersistentEntityResource resource = perAssembler.toResource(obj); + headers.set("Content-Location", resource.getId().getHref()); + return resource; } } } else if(prop.property.isMap()) { - PersistentEntity entity = repositories.getPersistentEntity(prop.propertyType); for(Map.Entry entry : ((Map)prop.propertyValue).entrySet()) { - BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), conversionService); + + BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), null); String sId = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); + if(propertyId.equals(sId)) { - PersistentEntityResource per = PersistentEntityResource.wrap(entity, - entry.getValue(), - repoRequest.getBaseUri()); - Link selfLink = entityLinks.linkForSingleResource(entity.getType(), sId).withSelfRel(); - per.add(selfLink); - headers.set("Content-Location", selfLink.getHref()); - return per; + + PersistentEntityResource resource = perAssembler.toResource(entry.getValue()); + headers.set("Content-Location", resource.getId().getHref()); + return resource; } } } else { @@ -234,11 +253,9 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes throw new IllegalArgumentException(new ResourceNotFoundException()); } }; - Resource responseResource = doWithReferencedProperty(repoRequest, - id, - property, - handler); - return resourceResponse(headers, responseResource, HttpStatus.OK); + + Resource responseResource = doWithReferencedProperty(repoRequest, id, property, handler); + return ControllerUtils.toResponseEntity(headers, responseResource, HttpStatus.OK); } @RequestMapping( @@ -263,7 +280,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes ResourceMapping entityMapping = repoRequest.getPersistentEntityResourceMapping(); String propName = entityMapping.getNameForPath(property); ResourceMapping propMapping = entityMapping.getResourceMappingFor(entityMapping.getNameForPath(property)); - PersistentProperty persistentProp = repoRequest.getPersistentEntity().getPersistentProperty(propName); + PersistentProperty persistentProp = repoRequest.getPersistentEntity().getPersistentProperty(propName); Class propType = (persistentProp.isCollectionLike() || persistentProp.isMap() ? persistentProp.getComponentType() : persistentProp.getType()); @@ -290,15 +307,14 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } } else if(resource.getContent() instanceof Map) { for(Map.Entry> entry : ((Map>)resource.getContent()).entrySet()) { - Link l = new Link(entry.getValue().getLink("self").getHref(), conversionService.convert(entry.getKey(), - String.class)); + Link l = new Link(entry.getValue().getLink("self").getHref(), entry.getKey().toString()); links.add(l); } } else { links.add(new Link(entityBaseUri.toString(), propRel)); } - return resourceResponse(null, new Resource(EMPTY_RESOURCE_LIST, links), HttpStatus.OK); + return ControllerUtils.toResponseEntity(null, new Resource(EMPTY_RESOURCE_LIST, links), HttpStatus.OK); } @RequestMapping( @@ -326,9 +342,9 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes Function> handler = new Function>() { @Override public Resource apply(ReferencedProperty prop) { if(prop.property.isCollectionLike()) { - Collection coll = new ArrayList(); + Collection coll = new ArrayList(); if("POST".equals(repoRequest.getRequest().getMethod())) { - coll.addAll((Collection)prop.propertyValue); + coll.addAll((Collection)prop.propertyValue); } for(Link l : incoming.getLinks()) { Object propVal = loadPropertyValue(prop.propertyType, l.getHref()); @@ -336,9 +352,9 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } prop.wrapper.setProperty(prop.property, coll); } else if(prop.property.isMap()) { - Map m = new HashMap(); + Map m = new HashMap(); if("POST".equals(repoRequest.getRequest().getMethod())) { - m.putAll((Map)prop.propertyValue); + m.putAll((Map)prop.propertyValue); } for(Link l : incoming.getLinks()) { Object propVal = loadPropertyValue(prop.propertyType, l.getHref()); @@ -358,9 +374,9 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes prop.wrapper.setProperty(prop.property, propVal); } - applicationContext.publishEvent(new BeforeLinkSaveEvent(prop.wrapper.getBean(), prop.propertyValue)); + publisher.publishEvent(new BeforeLinkSaveEvent(prop.wrapper.getBean(), prop.propertyValue)); Object result = repoMethodInvoker.save(prop.wrapper.getBean()); - applicationContext.publishEvent(new AfterLinkSaveEvent(result, prop.propertyValue)); + publisher.publishEvent(new AfterLinkSaveEvent(result, prop.propertyValue)); return null; } }; @@ -368,7 +384,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes id, property, handler); - return resourceResponse(null, EMPTY_RESOURCE, HttpStatus.CREATED); + return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.CREATED); } @RequestMapping( @@ -392,20 +408,20 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes return null; } if(prop.property.isCollectionLike()) { - Collection coll = new ArrayList(); - for(Object obj : (Collection)prop.propertyValue) { - BeanWrapper propValWrapper = BeanWrapper.create(obj, conversionService); - String s = (String)propValWrapper.getProperty(prop.entity.getIdProperty(), String.class, false); + Collection coll = new ArrayList(); + for(Object obj : (Collection) prop.propertyValue) { + BeanWrapper propValWrapper = BeanWrapper.create(obj, null); + String s = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); if(!propertyId.equals(s)) { coll.add(obj); } } prop.wrapper.setProperty(prop.property, coll); } else if(prop.property.isMap()) { - Map m = new HashMap(); + Map m = new HashMap(); for(Map.Entry entry : ((Map)prop.propertyValue).entrySet()) { - BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), conversionService); - String s = (String)propValWrapper.getProperty(prop.entity.getIdProperty(), String.class, false); + BeanWrapper propValWrapper = BeanWrapper.create(entry.getValue(), null); + String s = propValWrapper.getProperty(prop.entity.getIdProperty()).toString(); if(!propertyId.equals(s)) { m.put(entry.getKey(), entry.getValue()); } @@ -415,9 +431,9 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes prop.wrapper.setProperty(prop.property, null); } - applicationContext.publishEvent(new BeforeLinkDeleteEvent(prop.wrapper.getBean(), prop.propertyValue)); + publisher.publishEvent(new BeforeLinkDeleteEvent(prop.wrapper.getBean(), prop.propertyValue)); Object result = repoMethodInvoker.save(prop.wrapper.getBean()); - applicationContext.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); + publisher.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); return null; } }; @@ -426,7 +442,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes property, handler); - return resourceResponse(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); + return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); } private Link propertyReferenceLink(Resource resource, @@ -439,7 +455,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes private Object loadPropertyValue(Class type, String href) { String id = href.substring(href.lastIndexOf('/') + 1); - return domainClassConverter.convert(id, + return converter.convert(id, STRING_TYPE, TypeDescriptor.valueOf(type)); } @@ -454,21 +470,20 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes throw new NoSuchMethodException(); } - Object domainObj = domainClassConverter.convert(id, - STRING_TYPE, - TypeDescriptor.valueOf(repoRequest.getPersistentEntity() - .getType())); + Object domainObj = converter.convert(id, STRING_TYPE, + TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType())); + if(null == domainObj) { throw new ResourceNotFoundException(); } String propertyName = repoRequest.getPersistentEntityResourceMapping().getNameForPath(propertyPath); - PersistentProperty prop = repoRequest.getPersistentEntity().getPersistentProperty(propertyName); + PersistentProperty prop = repoRequest.getPersistentEntity().getPersistentProperty(propertyName); if(null == prop) { throw new ResourceNotFoundException(); } - BeanWrapper wrapper = BeanWrapper.create(domainObj, conversionService); + BeanWrapper wrapper = BeanWrapper.create(domainObj, null); Object propVal = wrapper.getProperty(prop); return handler.apply(new ReferencedProperty(prop, @@ -477,15 +492,17 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } private class ReferencedProperty { - final PersistentEntity entity; - final PersistentProperty property; - final Class propertyType; - final Object propertyValue; - final BeanWrapper wrapper; + + final PersistentEntity entity; + final PersistentProperty property; + final Class propertyType; + final Object propertyValue; + final BeanWrapperwrapper; - private ReferencedProperty(PersistentProperty property, + private ReferencedProperty(PersistentProperty property, Object propertyValue, - BeanWrapper wrapper) { + BeanWrapper wrapper) { + this.property = property; this.propertyValue = propertyValue; this.wrapper = wrapper; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java index 08d45784c..51f9cba70 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java @@ -1,19 +1,19 @@ package org.springframework.data.rest.webmvc; -import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; import static org.springframework.util.StringUtils.*; import java.util.ArrayList; import java.util.List; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; -import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.core.annotation.AnnotationUtils; 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.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.support.JpaHelper; import org.springframework.http.MediaType; import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor; @@ -36,9 +36,12 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping { private RepositoryRestConfiguration config; @Autowired(required = false) private JpaHelper jpaHelper; + + private final ResourceMappings mappings; - public RepositoryRestHandlerMapping() { + public RepositoryRestHandlerMapping(ResourceMappings mappings) { setOrder(Ordered.LOWEST_PRECEDENCE); + this.mappings = mappings; } @Override @@ -81,11 +84,9 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping { // Root request return super.lookupHandlerMethod(lookupPath, request); } - + for(Class domainType : repositories) { - RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(domainType); - ResourceMapping mapping = getResourceMapping(config, repoInfo); - if(mapping.getPath().equals(parts[0]) && mapping.isExported()) { + if (mappings.exportsMappingFor(domainType)) { return super.lookupHandlerMethod(lookupPath, request); } } @@ -94,10 +95,7 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping { } @Override protected boolean isHandler(Class beanType) { - return (RepositoryController.class.isAssignableFrom(beanType) - || RepositoryEntityController.class.isAssignableFrom(beanType) - || RepositoryPropertyReferenceController.class.isAssignableFrom(beanType) - || RepositorySearchController.class.isAssignableFrom(beanType)); + return AnnotationUtils.findAnnotation(beanType, RestController.class) != null; } @Override protected void extendInterceptors(List interceptors) { 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 ce77743cb..63b7d3a24 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 @@ -1,34 +1,44 @@ +/* + * Copyright 2012-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.webmvc; import static org.springframework.data.rest.core.util.UriUtils.*; import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; import java.net.URI; -import java.util.Enumeration; -import java.util.List; import javax.servlet.http.HttpServletRequest; -import org.springframework.data.domain.Page; +import org.springframework.core.convert.ConversionService; import org.springframework.data.mapping.PersistentEntity; 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.PagingAndSorting; import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; import org.springframework.hateoas.Link; -import org.springframework.web.util.UriComponentsBuilder; /** * @author Jon Brisbin + * @author Oliver Gierke */ +@SuppressWarnings("deprecation") class RepositoryRestRequest { - private final RepositoryRestConfiguration config; private final HttpServletRequest request; - private final PagingAndSorting pagingAndSorting; private final URI baseUri; - private final RepositoryInformation repoInfo; private final ResourceMapping repoMapping; private final Link repoLink; private final Object repository; @@ -39,14 +49,11 @@ class RepositoryRestRequest { public RepositoryRestRequest(RepositoryRestConfiguration config, Repositories repositories, HttpServletRequest request, - PagingAndSorting pagingAndSorting, URI baseUri, - RepositoryInformation repoInfo) { - this.config = config; + RepositoryInformation repoInfo, + ConversionService conversionService) { this.request = request; - this.pagingAndSorting = pagingAndSorting; this.baseUri = baseUri; - this.repoInfo = repoInfo; this.repoMapping = getResourceMapping(config, repoInfo); if(null == repoMapping || !repoMapping.isExported()) { this.repoLink = null; @@ -58,7 +65,7 @@ class RepositoryRestRequest { this.repoLink = new Link(buildUri(baseUri, repoMapping.getPath()).toString(), repoMapping.getRel()); this.repository = repositories.getRepositoryFor(repoInfo.getDomainType()); this.persistentEntity = repositories.getPersistentEntity(repoInfo.getDomainType()); - this.repoMethodInvoker = new RepositoryMethodInvoker(repository, repoInfo); + this.repoMethodInvoker = new RepositoryMethodInvoker(repository, repoInfo, conversionService); this.entityMapping = getResourceMapping(config, persistentEntity); } } @@ -67,18 +74,10 @@ class RepositoryRestRequest { return request; } - PagingAndSorting getPagingAndSorting() { - return pagingAndSorting; - } - URI getBaseUri() { return baseUri; } - RepositoryInformation getRepositoryInformation() { - return repoInfo; - } - ResourceMapping getRepositoryResourceMapping() { return repoMapping; } @@ -87,10 +86,6 @@ class RepositoryRestRequest { return repoLink; } - Object getRepository() { - return repository; - } - RepositoryMethodInvoker getRepositoryMethodInvoker() { return repoMethodInvoker; } @@ -102,40 +97,4 @@ class RepositoryRestRequest { ResourceMapping getPersistentEntityResourceMapping() { return entityMapping; } - - void addNextLink(Page page, List links) { - UriComponentsBuilder builder = UriComponentsBuilder.fromUri(baseUri); - // Add existing query parameters - addQueryParameters(request, builder); - - builder.queryParam(config.getPageParamName(), page.getNumber() + 1) - .queryParam(config.getLimitParamName(), pagingAndSorting.getPageSize()); - - links.add(new Link(builder.build().toString(), "page.next")); - } - - void addPrevLink(Page page, List links) { - UriComponentsBuilder builder = UriComponentsBuilder.fromUri(baseUri); - // Add existing query parameters - addQueryParameters(request, builder); - - builder.queryParam(config.getPageParamName(), page.getNumber() - 1) - .queryParam(config.getLimitParamName(), pagingAndSorting.getPageSize()); - - links.add(new Link(builder.build().toString(), "page.previous")); - } - - private void addQueryParameters(HttpServletRequest request, - UriComponentsBuilder builder) { - for(Enumeration names = request.getParameterNames(); names.hasMoreElements(); ) { - String name = names.nextElement(); - String value = request.getParameter(name); - if(name.equals(config.getPageParamName()) || name.equals(config.getLimitParamName())) { - continue; - } - - builder.queryParam(name, value); - } - } - } 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 228bfdb66..f9896504f 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 @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.webmvc; import java.net.URI; @@ -5,10 +20,10 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionService; 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.PagingAndSorting; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -16,9 +31,12 @@ import org.springframework.web.method.support.ModelAndViewContainer; /** * @author Jon Brisbin + * @author Oliver Gierke */ public class RepositoryRestRequestHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { - + + private final ConversionService conversionService; + @Autowired private RepositoryRestConfiguration config; @Autowired @@ -26,11 +44,14 @@ public class RepositoryRestRequestHandlerMethodArgumentResolver implements Handl @Autowired private RepositoryInformationHandlerMethodArgumentResolver repoInfoResolver; @Autowired - private PagingAndSortingMethodArgumentResolver pagingAndSortingResolver; - @Autowired private BaseUriMethodArgumentResolver baseUriResolver; + + public RepositoryRestRequestHandlerMethodArgumentResolver(ConversionService conversionService) { + this.conversionService = conversionService; + } - @Override public boolean supportsParameter(MethodParameter parameter) { + @Override + public boolean supportsParameter(MethodParameter parameter) { return RepositoryRestRequest.class.isAssignableFrom(parameter.getParameterType()); } @@ -39,25 +60,11 @@ public class RepositoryRestRequestHandlerMethodArgumentResolver implements Handl ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - PagingAndSorting pagingAndSorting = (PagingAndSorting)pagingAndSortingResolver.resolveArgument(parameter, - mavContainer, - webRequest, - binderFactory); - RepositoryInformation repoInfo = (RepositoryInformation)repoInfoResolver.resolveArgument(parameter, - mavContainer, - webRequest, - binderFactory); - URI baseUri = (URI)baseUriResolver.resolveArgument(parameter, - mavContainer, - webRequest, - binderFactory); + + URI baseUri = (URI) baseUriResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + RepositoryInformation repoInfo = repoInfoResolver.resolveArgument(parameter, mavContainer, webRequest, + binderFactory); - return new RepositoryRestRequest(config, - repositories, - webRequest.getNativeRequest(HttpServletRequest.class), - pagingAndSorting, - baseUri, - repoInfo); + 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 730672f54..268fa0eb2 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,26 +1,45 @@ +/* + * 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.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; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; -import org.springframework.core.MethodParameter; -import org.springframework.core.convert.ConversionService; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.repository.support.DomainClassConverter; +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.PagingAndSorting; import org.springframework.data.rest.repository.invoke.RepositoryMethod; import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; -import org.springframework.hateoas.EntityLinks; +import org.springframework.data.rest.repository.support.ResourceMappingUtils; +import org.springframework.data.rest.webmvc.support.BaseUriLinkBuilder; +import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkBuilder; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; @@ -31,21 +50,27 @@ import org.springframework.web.bind.annotation.ResponseBody; /** * @author Jon Brisbin + * @author Oliver Gierke */ -public class RepositorySearchController extends AbstractRepositoryRestController { +@RestController +@SuppressWarnings("deprecation") +class RepositorySearchController extends AbstractRepositoryRestController { private static final String BASE_MAPPING = "/{repository}/search"; + private final Repositories repositories; + private final RepositoryRestConfiguration config; + + @Autowired public RepositorySearchController(Repositories repositories, RepositoryRestConfiguration config, - DomainClassConverter domainClassConverter, - ConversionService conversionService, - EntityLinks entityLinks) { - super(repositories, - config, - domainClassConverter, - conversionService, - entityLinks); + PagedResourcesAssembler assembler, + PersistentEntityResourceAssembler perAssembler) { + + super(assembler, perAssembler); + + this.repositories = repositories; + this.config = config; } @RequestMapping( @@ -66,6 +91,24 @@ public class RepositorySearchController extends AbstractRepositoryRestController } return new Resource(Collections.emptyList(), links); } + + 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()) { + continue; + } + links + .add(linkBuilder.slash(methodMapping.getPath()).withRel(repoMapping.getRel() + "." + methodMapping.getRel())); + } + return links; + } @RequestMapping( value = BASE_MAPPING + "/{method}", @@ -78,7 +121,7 @@ public class RepositorySearchController extends AbstractRepositoryRestController @ResponseBody public ResourceSupport query(final RepositoryRestRequest repoRequest, @PathVariable String repository, - @PathVariable String method) + @PathVariable String method, Pageable pageable) throws ResourceNotFoundException { RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); if(repoMethodInvoker.getQueryMethods().isEmpty()) { @@ -100,50 +143,15 @@ public class RepositorySearchController extends AbstractRepositoryRestController throw new ResourceNotFoundException(); } } + + + Map rawParameters = repoRequest.getRequest().getParameterMap(); + Object result = repoMethodInvoker.invokeQueryMethod(repoMethod, pageable, rawParameters); + + Link baseLink = linkTo(methodOn(RepositorySearchController.class). // + queryCompact(repoRequest, repository, methodName, pageable)).withSelfRel(); - PagingAndSorting pageSort = repoRequest.getPagingAndSorting(); - List methodParams = repoMethod.getParameters(); - Object[] paramValues = new Object[methodParams.size()]; - if(!methodParams.isEmpty()) { - for(int i = 0; i < paramValues.length; i++) { - MethodParameter param = methodParams.get(i); - if(Pageable.class.isAssignableFrom(param.getParameterType())) { - paramValues[i] = new PageRequest(pageSort.getPageNumber(), - pageSort.getPageSize(), - pageSort.getSort()); - } else if(Sort.class.isAssignableFrom(param.getParameterType())) { - paramValues[i] = pageSort.getSort(); - } else { - String paramName = repoMethod.getParameterNames().get(i); - String[] queryParamVals = repoRequest.getRequest().getParameterValues(paramName); - if(null == queryParamVals) { - if(paramName.startsWith("arg")) { - throw new IllegalArgumentException("No @Param annotation found on query method " - + repoMethod.getMethod().getName() - + " for parameter " + param.getParameterName()); - } else { - throw new IllegalArgumentException("No query parameter specified for " - + repoMethod.getMethod().getName() + " param '" - + paramName + "'"); - } - } - paramValues[i] = methodParameterConversionService.convert(queryParamVals, param); - } - } - } - - Object result = repoMethodInvoker.invokeQueryMethod(repoMethod, paramValues); - Link prevLink = null; - Link nextLink = null; - if(result instanceof Page) { - if(((Page)result).hasPreviousPage() && pageSort.getPageNumber() > 0) { - prevLink = searchLink(repoRequest, 0, method, "page.previous"); - } - if(((Page)result).hasNextPage()) { - nextLink = searchLink(repoRequest, 1, method, "page.next"); - } - } - return resultToResources(repoRequest, result, new ArrayList(), prevLink, nextLink); + return resultToResources(result, baseLink); } @RequestMapping( @@ -156,11 +164,12 @@ public class RepositorySearchController extends AbstractRepositoryRestController @ResponseBody public ResourceSupport queryCompact(RepositoryRestRequest repoRequest, @PathVariable String repository, - @PathVariable String method) + @PathVariable String method, + Pageable pageable) throws ResourceNotFoundException { List links = new ArrayList(); - ResourceSupport resource = query(repoRequest, repository, method); + ResourceSupport resource = query(repoRequest, repository, method, pageable); links.addAll(resource.getLinks()); if(resource instanceof Resources && ((Resources) resource).getContent() != null) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java index f9eb55533..7f940651d 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandler.java @@ -24,6 +24,7 @@ import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceProcessor; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.hateoas.mvc.HeaderLinksResponseEntity; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; @@ -58,6 +59,7 @@ public class ResourceProcessorHandlerMethodReturnValueHandler implements Handler private final HandlerMethodReturnValueHandler delegate; private final List processors; + private boolean rootLinksAsHeaders = false; /** * Creates a new {@link ResourceProcessorHandlerMethodReturnValueHandler} using the given delegate to eventually @@ -95,6 +97,13 @@ public class ResourceProcessorHandlerMethodReturnValueHandler implements Handler Collections.sort(this.processors, AnnotationAwareOrderComparator.INSTANCE); } + + /** + * @param rootLinksAsHeaders the rootLinksAsHeaders to set + */ + public void setRootLinksAsHeaders(boolean rootLinksAsHeaders) { + this.rootLinksAsHeaders = rootLinksAsHeaders; + } /* * (non-Javadoc) @@ -159,14 +168,14 @@ public class ResourceProcessorHandlerMethodReturnValueHandler implements Handler ReflectionUtils.setField(CONTENT_FIELD, resources, result); } - Object result = invokeProcessorsFor(value, targetType); + ResourceSupport result = (ResourceSupport) invokeProcessorsFor(value, targetType); delegate.handleReturnValue(rewrapResult(result, returnValue), returnType, mavContainer, webRequest); } /** * Invokes all registered {@link ResourceProcessor}s registered for the given {@link TypeInformation}. * - * @param value the object to process + * @param value the object to process * @param targetType * @return */ @@ -192,19 +201,28 @@ public class ResourceProcessorHandlerMethodReturnValueHandler implements Handler * @param originalValue the original input value. * @return */ - static Object rewrapResult(Object newBody, Object originalValue) { + Object rewrapResult(ResourceSupport newBody, Object originalValue) { if (!(originalValue instanceof HttpEntity)) { return newBody; } + + HttpEntity entity = null; if (originalValue instanceof ResponseEntity) { ResponseEntity source = (ResponseEntity) originalValue; - return new ResponseEntity(newBody, source.getHeaders(), source.getStatusCode()); + entity = new ResponseEntity(newBody, source.getHeaders(), source.getStatusCode()); } else { HttpEntity source = (HttpEntity) originalValue; - return new HttpEntity(newBody, source.getHeaders()); + entity = new HttpEntity(newBody, source.getHeaders()); } + + return addLinksToHeaderWrapper(entity); + } + + private HttpEntity addLinksToHeaderWrapper(HttpEntity entity) { + + return rootLinksAsHeaders ? HeaderLinksResponseEntity.wrap(entity) : entity; } /** diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestController.java new file mode 100644 index 000000000..9ba86c5f7 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RestController.java @@ -0,0 +1,35 @@ +/* + * 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.webmvc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +/** + * @author Oliver Gierke + */ +@Documented +@Component +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +public @interface RestController { + +} 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 d60ace3cf..c72f11177 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 @@ -1,47 +1,65 @@ +/* + * Copyright 2012-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.webmvc.config; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import org.springframework.context.annotation.Lazy; import org.springframework.core.convert.support.ConfigurableConversionService; 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.convert.ISO8601DateConverter; import org.springframework.data.rest.convert.UUIDConverter; import org.springframework.data.rest.repository.UriDomainClassConverter; import org.springframework.data.rest.repository.context.AnnotatedHandlerBeanPostProcessor; -import org.springframework.data.rest.repository.context.RepositoriesFactoryBean; import org.springframework.data.rest.repository.context.ValidatingRepositoryEventListener; -import org.springframework.data.rest.repository.json.Jackson2DatatypeHelper; -import org.springframework.data.rest.repository.json.PersistentEntityJackson2Module; -import org.springframework.data.rest.repository.json.PersistentEntityToJsonSchemaConverter; +import org.springframework.data.rest.repository.mapping.ResourceMappings; import org.springframework.data.rest.repository.support.DomainObjectMerger; import org.springframework.data.rest.webmvc.BaseUriMethodArgumentResolver; -import org.springframework.data.rest.webmvc.PagingAndSortingMethodArgumentResolver; +import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler; import org.springframework.data.rest.webmvc.PersistentEntityResourceHandlerMethodArgumentResolver; -import org.springframework.data.rest.webmvc.RepositoryController; -import org.springframework.data.rest.webmvc.RepositoryEntityController; import org.springframework.data.rest.webmvc.RepositoryInformationHandlerMethodArgumentResolver; -import org.springframework.data.rest.webmvc.RepositoryPropertyReferenceController; 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.RepositorySearchController; +import org.springframework.data.rest.webmvc.RestController; import org.springframework.data.rest.webmvc.ServerHttpRequestMethodArgumentResolver; import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter; +import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper; +import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module; +import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter; import org.springframework.data.rest.webmvc.support.JpaHelper; import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks; import org.springframework.data.rest.webmvc.support.ValidationExceptionHandler; +import org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.RelProvider; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; @@ -51,33 +69,41 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + /** * Main application configuration for Spring Data REST. To customize how the exporter works, subclass this and override * any of the {@literal configure*} methods. *

* Any XML files located in the classpath under the {@literal META-INF/spring-data-rest/} path will be automatically * found and loaded into this {@link org.springframework.context.ApplicationContext}. - * + * * @author Jon Brisbin + * @author Oliver Gierke */ @Configuration +@ComponentScan(basePackageClasses = RestController.class, includeFilters = @Filter(RestController.class), useDefaultFilters = false) @ImportResource("classpath*:META-INF/spring-data-rest/**/*.xml") -public class RepositoryRestMvcConfiguration { +public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebConfiguration { private static final boolean IS_JAVAX_VALIDATION_AVAILABLE = ClassUtils.isPresent( - "javax.validation.ConstraintViolationException", - RepositoryRestMvcConfiguration.class.getClassLoader() - ); - private static final boolean IS_JPA_AVAILABLE = ClassUtils.isPresent( - "javax.persistence.EntityManager", - RepositoryRestMvcConfiguration.class.getClassLoader() - ); + "javax.validation.ConstraintViolationException", RepositoryRestMvcConfiguration.class.getClassLoader()); + private static final boolean IS_JPA_AVAILABLE = ClassUtils.isPresent("javax.persistence.EntityManager", + RepositoryRestMvcConfiguration.class.getClassLoader()); - @Bean public RepositoriesFactoryBean repositories() { - return new RepositoriesFactoryBean(); + @Autowired ListableBeanFactory beanFactory; + + @Bean + public Repositories repositories() { + return new Repositories(beanFactory); } - @Bean public DefaultFormattingConversionService defaultConversionService() { + @Bean + @Qualifier + public DefaultFormattingConversionService defaultConversionService() { + DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); conversionService.addConverter(UUIDConverter.INSTANCE); conversionService.addConverter(ISO8601DateConverter.INSTANCE); @@ -85,34 +111,41 @@ public class RepositoryRestMvcConfiguration { return conversionService; } - @Bean public DomainClassConverter domainClassConverter() { + @Bean + public DomainClassConverter domainClassConverter() { return new DomainClassConverter(defaultConversionService()); } - @Bean public UriDomainClassConverter uriDomainClassConverter() { + @Bean + public UriDomainClassConverter uriDomainClassConverter() { return new UriDomainClassConverter(); } /** - * {@link org.springframework.context.ApplicationListener} implementation for invoking {@link - * org.springframework.validation.Validator} instances assigned to specific domain types. + * {@link org.springframework.context.ApplicationListener} implementation for invoking + * {@link org.springframework.validation.Validator} instances assigned to specific domain types. */ - @Bean public ValidatingRepositoryEventListener validatingRepositoryEventListener() { + @Bean + public ValidatingRepositoryEventListener validatingRepositoryEventListener() { ValidatingRepositoryEventListener listener = new ValidatingRepositoryEventListener(); configureValidatingRepositoryEventListener(listener); return listener; } - @Bean @Lazy public ValidationExceptionHandler validationExceptionHandler() { - if(IS_JAVAX_VALIDATION_AVAILABLE) { + @Bean + @Lazy + public ValidationExceptionHandler validationExceptionHandler() { + if (IS_JAVAX_VALIDATION_AVAILABLE) { return new ValidationExceptionHandler(); } else { return null; } } - @Bean @Lazy public JpaHelper jpaHelper() { - if(IS_JPA_AVAILABLE) { + @Bean + @Lazy + public JpaHelper jpaHelper() { + if (IS_JPA_AVAILABLE) { return new JpaHelper(); } else { return null; @@ -122,167 +155,95 @@ public class RepositoryRestMvcConfiguration { /** * Main configuration for the REST exporter. */ - @Bean public RepositoryRestConfiguration config() { + @Bean + public RepositoryRestConfiguration config() { RepositoryRestConfiguration config = new RepositoryRestConfiguration(); configureRepositoryRestConfiguration(config); return config; } /** - * {@link org.springframework.beans.factory.config.BeanPostProcessor} to turn beans annotated as {@link - * org.springframework.data.rest.repository.annotation.RepositoryEventHandler}s. - * + * {@link org.springframework.beans.factory.config.BeanPostProcessor} to turn beans annotated as + * {@link org.springframework.data.rest.repository.annotation.RepositoryEventHandler}s. + * * @return */ - @Bean public AnnotatedHandlerBeanPostProcessor annotatedHandlerBeanPostProcessor() { + @Bean + public AnnotatedHandlerBeanPostProcessor annotatedHandlerBeanPostProcessor() { return new AnnotatedHandlerBeanPostProcessor(); } /** * For merging incoming objects materialized from JSON with existing domain objects loaded from the repository. - * + * * @return - * * @throws Exception */ - @Bean public DomainObjectMerger domainObjectMerger() throws Exception { - return new DomainObjectMerger( - repositories().getObject(), - defaultConversionService() - ); - } - - /** - * The controller that handles top-level requests for listing what repositories are available. - * - * @return - * - * @throws Exception - */ - @Bean public RepositoryController repositoryController() throws Exception { - return new RepositoryController( - repositories().getObject(), - config(), - domainClassConverter(), - defaultConversionService(), - entityLinks() - ); - } - - /** - * The controller responsible for handling requests to display or those that modify an entity. - * - * @return - * - * @throws Exception - */ - @Bean public RepositoryEntityController repositoryEntityController() throws Exception { - return new RepositoryEntityController( - repositories().getObject(), - config(), - domainClassConverter(), - defaultConversionService(), - entityLinks() - ); - } - - /** - * The controller responsible for managing links of property references. - * - * @return - * - * @throws Exception - */ - @Bean public RepositoryPropertyReferenceController propertyReferenceController() throws Exception { - return new RepositoryPropertyReferenceController( - repositories().getObject(), - config(), - domainClassConverter(), - defaultConversionService(), - entityLinks() - ); - } - - /** - * The controller responsible for performing searches. - * - * @return - * - * @throws Exception - */ - @Bean public RepositorySearchController repositorySearchController() throws Exception { - return new RepositorySearchController( - repositories().getObject(), - config(), - domainClassConverter(), - defaultConversionService(), - entityLinks() - ); + @Bean + public DomainObjectMerger domainObjectMerger() throws Exception { + return new DomainObjectMerger(repositories(), defaultConversionService()); } /** * Resolves the base {@link java.net.URI} under which this application is configured. - * + * * @return */ - @Bean public BaseUriMethodArgumentResolver baseUriMethodArgumentResolver() { - return new BaseUriMethodArgumentResolver(); + @Bean + public BaseUriMethodArgumentResolver baseUriMethodArgumentResolver() { + return new BaseUriMethodArgumentResolver(config()); } /** * Resolves the {@link org.springframework.data.repository.core.RepositoryInformation} for this request. - * + * * @return */ - @Bean public RepositoryInformationHandlerMethodArgumentResolver repoInfoMethodArgumentResolver() { + @Bean + public RepositoryInformationHandlerMethodArgumentResolver repoInfoMethodArgumentResolver() { return new RepositoryInformationHandlerMethodArgumentResolver(); } /** - * Resolves the paging and sorting information from the query parameters based on the current configuration settings. - * + * Turns an {@link javax.servlet.http.HttpServletRequest} into a + * {@link org.springframework.http.server.ServerHttpRequest}. + * * @return */ - @Bean public PagingAndSortingMethodArgumentResolver pagingAndSortingMethodArgumentResolver() { - return new PagingAndSortingMethodArgumentResolver(); - } - - /** - * Turns an {@link javax.servlet.http.HttpServletRequest} into a {@link org.springframework.http.server.ServerHttpRequest}. - * - * @return - */ - @Bean public ServerHttpRequestMethodArgumentResolver serverHttpRequestMethodArgumentResolver() { + @Bean + public ServerHttpRequestMethodArgumentResolver serverHttpRequestMethodArgumentResolver() { return new ServerHttpRequestMethodArgumentResolver(); } /** * A convenience resolver that pulls together all the information needed to service a request. - * + * * @return */ - @Bean public RepositoryRestRequestHandlerMethodArgumentResolver repoRequestArgumentResolver() { - return new RepositoryRestRequestHandlerMethodArgumentResolver(); + @Bean + public RepositoryRestRequestHandlerMethodArgumentResolver repoRequestArgumentResolver() { + return new RepositoryRestRequestHandlerMethodArgumentResolver(defaultConversionService()); } /** * A special {@link org.springframework.hateoas.EntityLinks} implementation that takes repository and current * configuration into account when generating links. - * + * * @return - * * @throws Exception */ - @Bean public EntityLinks entityLinks() throws Exception { - return new RepositoryEntityLinks(repositories().getObject(), config()); + @Bean + public EntityLinks entityLinks() { + return new RepositoryEntityLinks(repositories(), resourceMappings(), config()); } /** * Reads incoming JSON into an entity. - * + * * @return */ - @Bean public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver() { + @Bean + public PersistentEntityResourceHandlerMethodArgumentResolver persistentEntityArgumentResolver() { List> messageConverters = defaultMessageConverters(); configureHttpMessageConverters(messageConverters); @@ -290,20 +251,22 @@ public class RepositoryRestMvcConfiguration { } /** - * Turns a domain class into a {@link org.springframework.data.rest.repository.json.JsonSchema}. - * + * Turns a domain class into a {@link org.springframework.data.rest.webmvc.json.JsonSchema}. + * * @return */ - @Bean public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() { + @Bean + public PersistentEntityToJsonSchemaConverter jsonSchemaConverter() { return new PersistentEntityToJsonSchemaConverter(); } /** * The Jackson {@link ObjectMapper} used internally. - * + * * @return */ - @Bean public ObjectMapper objectMapper() { + @Bean + public ObjectMapper objectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); // Our special PersistentEntityResource Module @@ -317,37 +280,43 @@ public class RepositoryRestMvcConfiguration { /** * The {@link HttpMessageConverter} used by Spring MVC to read and write JSON data. - * + * * @return */ - @Bean public MappingJackson2HttpMessageConverter jacksonHttpMessageConverter() { + @Bean + public MappingJackson2HttpMessageConverter jacksonHttpMessageConverter() { MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(); jacksonConverter.setObjectMapper(objectMapper()); - jacksonConverter.setSupportedMediaTypes(Arrays.asList( - MediaType.APPLICATION_JSON, - MediaType.valueOf("application/schema+json"), - MediaType.valueOf("application/x-spring-data-verbose+json"), - MediaType.valueOf("application/x-spring-data-compact+json") - )); + jacksonConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON, + MediaType.valueOf("application/schema+json"), MediaType.valueOf("application/x-spring-data-verbose+json"), + MediaType.valueOf("application/x-spring-data-compact+json"))); return jacksonConverter; } /** * The {@link HttpMessageConverter} used to create {@literal text/uri-list} responses. - * + * * @return */ - @Bean public UriListHttpMessageConverter uriListHttpMessageConverter() { + @Bean + public UriListHttpMessageConverter uriListHttpMessageConverter() { return new UriListHttpMessageConverter(); } + @Bean + public PersistentEntityResourceAssembler persistentEntityResourceAssembler() { + return new PersistentEntityResourceAssembler(repositories(), entityLinks()); + } + /** - * Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in - * the provided controller classes. - * + * Special {@link org.springframework.web.servlet.HandlerAdapter} that only recognizes handler methods defined in the + * provided controller classes. + * * @return */ - @Bean public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() { + @Bean + public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() { + List> messageConverters = defaultMessageConverters(); configureHttpMessageConverters(messageConverters); @@ -359,30 +328,47 @@ public class RepositoryRestMvcConfiguration { } /** - * Special {@link org.springframework.web.servlet.HandlerMapping} that only recognizes handler methods defined in - * the provided controller classes. - * + * Special {@link org.springframework.web.servlet.HandlerMapping} that only recognizes handler methods defined in the + * provided controller classes. + * * @return */ - @Bean public RequestMappingHandlerMapping repositoryExporterHandlerMapping() { - return new RepositoryRestHandlerMapping(); + @Bean + public RequestMappingHandlerMapping repositoryExporterHandlerMapping() { + return new RepositoryRestHandlerMapping(resourceMappings()); + } + + @Bean + public ResourceMappings resourceMappings() { + + Repositories repositories = repositories(); + RepositoryRestConfiguration config = config(); + + try { + RelProvider relProvider = beanFactory.getBean(RelProvider.class); + return new ResourceMappings(config, repositories, relProvider); + } catch (NoSuchBeanDefinitionException e) { + return new ResourceMappings(config, repositories); + } } /** * Jackson module responsible for intelligently serializing and deserializing JSON that corresponds to an entity. - * + * * @return */ - @Bean public Module persistentEntityJackson2Module() { - return new PersistentEntityJackson2Module(defaultConversionService()); + @Bean + public Module persistentEntityJackson2Module() { + return new PersistentEntityJackson2Module(resourceMappings()); } /** * Bean for looking up methods annotated with {@link org.springframework.web.bind.annotation.ExceptionHandler}. - * + * * @return */ - @Bean public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() { + @Bean + public ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver() { ExceptionHandlerExceptionResolver er = new ExceptionHandlerExceptionResolver(); er.setCustomArgumentResolvers(defaultMethodArgumentResolvers()); @@ -403,67 +389,51 @@ public class RepositoryRestMvcConfiguration { } private List defaultMethodArgumentResolvers() { - return Arrays.asList(baseUriMethodArgumentResolver(), - pagingAndSortingMethodArgumentResolver(), - serverHttpRequestMethodArgumentResolver(), - repoInfoMethodArgumentResolver(), - repoRequestArgumentResolver(), - persistentEntityArgumentResolver()); + return Arrays.asList(baseUriMethodArgumentResolver(), pageableResolver(), sortResolver(), + serverHttpRequestMethodArgumentResolver(), repoInfoMethodArgumentResolver(), repoRequestArgumentResolver(), + persistentEntityArgumentResolver()); } /** * Override this method to add additional configuration. - * - * @param config - * Main configuration bean. + * + * @param config Main configuration bean. */ - protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { - } + protected void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {} /** * Override this method to add your own converters. - * - * @param conversionService - * Default ConversionService bean. + * + * @param conversionService Default ConversionService bean. */ - protected void configureConversionService(ConfigurableConversionService conversionService) { - } + protected void configureConversionService(ConfigurableConversionService conversionService) {} /** * Override this method to add validators manually. - * - * @param validatingListener - * The {@link org.springframework.context.ApplicationListener} responsible for invoking {@link - * org.springframework.validation.Validator} instances. + * + * @param validatingListener The {@link org.springframework.context.ApplicationListener} responsible for invoking + * {@link org.springframework.validation.Validator} instances. */ - protected void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) { - } + protected void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {} /** * Configure the {@link ExceptionHandlerExceptionResolver}. - * - * @param exceptionResolver - * The default exception resolver on which you can add custom argument resolvers. + * + * @param exceptionResolver The default exception resolver on which you can add custom argument resolvers. */ - protected void configureExceptionHandlerExceptionResolver(ExceptionHandlerExceptionResolver exceptionResolver) { - } + protected void configureExceptionHandlerExceptionResolver(ExceptionHandlerExceptionResolver exceptionResolver) {} /** * Configure the available {@link HttpMessageConverter}s by adding your own. - * - * @param messageConverters - * The converters to be used by the system. + * + * @param messageConverters The converters to be used by the system. */ - protected void configureHttpMessageConverters(List> messageConverters) { - } + protected void configureHttpMessageConverters(List> messageConverters) {} /** * Configure the Jackson {@link ObjectMapper} directly. - * - * @param objectMapper - * The {@literal ObjectMapper} to be used by the system. + * + * @param objectMapper The {@literal ObjectMapper} to be used by the system. */ - protected void configureJacksonObjectMapper(ObjectMapper objectMapper) { - } - + protected void configureJacksonObjectMapper(ObjectMapper objectMapper) {} } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/Jackson2DatatypeHelper.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/Jackson2DatatypeHelper.java similarity index 96% rename from spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/Jackson2DatatypeHelper.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/Jackson2DatatypeHelper.java index 50e58a222..6be7fa07c 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/Jackson2DatatypeHelper.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/Jackson2DatatypeHelper.java @@ -1,4 +1,4 @@ -package org.springframework.data.rest.repository.json; +package org.springframework.data.rest.webmvc.json; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/JsonSchema.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java similarity index 97% rename from spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/JsonSchema.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java index e466e9c87..ed059f7f3 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/JsonSchema.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java @@ -1,4 +1,4 @@ -package org.springframework.data.rest.repository.json; +package org.springframework.data.rest.webmvc.json; import java.util.ArrayList; import java.util.HashMap; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java similarity index 62% rename from spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityJackson2Module.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index 1723ce979..1abd88089 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -1,8 +1,6 @@ -package org.springframework.data.rest.repository.json; +package org.springframework.data.rest.webmvc.json; import static org.springframework.beans.BeanUtils.*; -import static org.springframework.data.rest.core.util.UriUtils.*; -import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; import java.io.IOException; import java.net.URI; @@ -15,6 +13,28 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.AssociationHandler; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.model.BeanWrapper; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.repository.PersistentEntityResource; +import org.springframework.data.rest.repository.UriDomainClassConverter; +import org.springframework.data.rest.repository.mapping.ResourceMappings; +import org.springframework.data.rest.repository.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.support.RepositoryLinkBuilder; +import org.springframework.hateoas.Link; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.util.Assert; + import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -26,26 +46,6 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.AssociationHandler; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PropertyHandler; -import org.springframework.data.mapping.model.BeanWrapper; -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.PersistentEntityResource; -import org.springframework.data.rest.repository.UriDomainClassConverter; -import org.springframework.hateoas.Link; -import org.springframework.http.converter.HttpMessageNotReadableException; /** * @author Jon Brisbin @@ -55,54 +55,45 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init private static final long serialVersionUID = -7289265674870906323L; private static final Logger LOG = LoggerFactory.getLogger(PersistentEntityJackson2Module.class); private static final TypeDescriptor URI_TYPE = TypeDescriptor.valueOf(URI.class); - private final ConversionService conversionService; @Autowired private Repositories repositories; @Autowired private RepositoryRestConfiguration config; @Autowired private UriDomainClassConverter uriDomainClassConverter; + private final ResourceMappings mappings; - public PersistentEntityJackson2Module(ConversionService conversionService) { + public PersistentEntityJackson2Module(ResourceMappings resourceMappings) { + super(new Version(1, 1, 0, "BUILD-SNAPSHOT", "org.springframework.data.rest", "jackson-module")); - this.conversionService = conversionService; - + + this.mappings = resourceMappings; addSerializer(new ResourceSerializer()); } - public static boolean maybeAddAssociationLink(Repositories repositories, - RepositoryRestConfiguration config, - URI baseEntityUri, - RepositoryInformation repoInfo, - ResourceMapping entityMapping, - ResourceMapping propertyMapping, + public static boolean maybeAddAssociationLink(RepositoryLinkBuilder builder, + ResourceMappings mappings, PersistentProperty persistentProperty, List links) { - Class propertyType = persistentProperty.getType(); - if(persistentProperty.isCollectionLike() || persistentProperty.isArray()) { - propertyType = persistentProperty.getComponentType(); - } - - String propertyPath = (null != propertyMapping - ? propertyMapping.getPath() - : persistentProperty.getName()); - // In case a property mapping is specified but no path is set - if(null == propertyPath) { - propertyPath = persistentProperty.getName(); - } - String propertyRel = formatRel(config, repoInfo, persistentProperty); - if(repositories.hasRepositoryFor(propertyType)) { - // This is a managed type, generate a Link - RepositoryInformation linkedRepoInfo = repositories.getRepositoryInformationFor(propertyType); - ResourceMapping linkedRepoMapping = getResourceMapping(config, linkedRepoInfo); - if(linkedRepoMapping.isExported()) { - URI uri = buildUri(baseEntityUri, propertyPath); - Link l = new Link(uri.toString(), propertyRel); - links.add(l); - // This is an association. We added a Link. - return true; - } + + Assert.isTrue(persistentProperty.isAssociation(), "PersistentProperty must be an association!"); + ResourceMetadata metadata = mappings.getMappingFor(persistentProperty.getOwner().getType()); + + if (!metadata.isManaged(persistentProperty)) { + return false; } + + metadata = mappings.getMappingFor(persistentProperty.getActualType()); + + if(metadata.isExported()) { + + String propertyRel = String.format("%s.%s", metadata.getSingleResourceRel(), persistentProperty.getName()); + links.add(builder.slash(persistentProperty.getName()).withRel(propertyRel)); + + // This is an association. We added a Link. + return true; + } + // This is not an association. No Link was added. return false; } @@ -136,8 +127,9 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init DeserializationContext ctxt) throws IOException, JsonProcessingException { Object entity = instantiateClass(getValueClass()); - BeanWrapper wrapper = BeanWrapper.create(entity, conversionService); - ResourceMapping domainMapping = config.getResourceMappingForDomainType(getValueClass()); + BeanWrapper wrapper = BeanWrapper.create(entity, null); + + ResourceMetadata metadata = mappings.getMappingFor(getValueClass()); for(JsonToken tok = jp.nextToken(); tok != JsonToken.END_OBJECT; tok = jp.nextToken()) { String name = jp.getCurrentName(); @@ -160,18 +152,7 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init PersistentProperty persistentProperty = persistentEntity.getPersistentProperty(name); if(null == persistentProperty) { - String errMsg = "Property '" + name + "' not found for entity " + getValueClass().getName(); - if(null == domainMapping) { - throw new HttpMessageNotReadableException(errMsg); - } - String propertyName = domainMapping.getNameForPath(name); - if(null == propertyName) { - throw new HttpMessageNotReadableException(errMsg); - } - persistentProperty = persistentEntity.getPersistentProperty(propertyName); - if(null == persistentProperty) { - throw new HttpMessageNotReadableException(errMsg); - } + continue; } Object val = null; @@ -199,7 +180,7 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init // The method of doing that varies based on the type of the property. if(persistentProperty.isCollectionLike()) { Class> ctype = (Class>) persistentProperty.getType(); - Collection c = (Collection) wrapper.getProperty(persistentProperty, ctype, false); + Collection c = (Collection) wrapper.getProperty(persistentProperty); if(null == c || c == Collections.EMPTY_LIST || c == Collections.EMPTY_SET) { if(Collection.class.isAssignableFrom(ctype)) { c = new ArrayList(); @@ -222,7 +203,7 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init } } else if(persistentProperty.isMap()) { Class> mtype = (Class>)persistentProperty.getType(); - Map m = (Map) wrapper.getProperty(persistentProperty, mtype, false); + Map m = (Map) wrapper.getProperty(persistentProperty); if(null == m || m == Collections.EMPTY_MAP) { m = new HashMap(); } @@ -278,18 +259,13 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init Object obj = resource.getContent(); - final PersistentEntity persistentEntity = resource.getPersistentEntity(); - final ResourceMapping entityMapping = getResourceMapping(config, persistentEntity); + final PersistentEntity entity = resource.getPersistentEntity(); - final RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(persistentEntity.getType()); - final ResourceMapping repoMapping = getResourceMapping(config, repoInfo); - - final BeanWrapper wrapper = BeanWrapper.create(obj, conversionService); - final Object entityId = wrapper.getProperty(persistentEntity.getIdProperty()); - - final URI baseEntityUri = buildUri(resource.getBaseUri(), - repoMapping.getPath(), - entityId.toString()); + final BeanWrapper wrapper = BeanWrapper.create(obj, null); + final Object entityId = wrapper.getProperty(entity.getIdProperty()); + final ResourceMappings mappings = new ResourceMappings(config, repositories); + final ResourceMetadata metadata = mappings.getMappingFor(entity.getType()); + final RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, config.getBaseUri()).slash(entityId); final List links = new ArrayList(); // Start with ResourceProcessor-added links @@ -297,32 +273,23 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init jgen.writeStartObject(); try { - persistentEntity.doWithProperties(new PropertyHandler() { - @Override public void doWithPersistentProperty(PersistentProperty persistentProperty) { - if(persistentProperty.isIdProperty() && !config.isIdExposedFor(persistentEntity.getType())) { + entity.doWithProperties(new PropertyHandler() { + @Override public void doWithPersistentProperty(PersistentProperty property) { + + boolean idAvailableAndShallNotBeExposed = property.isIdProperty() && !config.isIdExposedFor(entity.getType()); + + if(idAvailableAndShallNotBeExposed) { return; } - ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName()); - if(null != propertyMapping && !propertyMapping.isExported()) { - return; - } - - if(persistentProperty.isEntity() && maybeAddAssociationLink(repositories, - config, - baseEntityUri, - repoInfo, - entityMapping, - propertyMapping, - persistentProperty, - links)) { + + if (property.isEntity() && maybeAddAssociationLink(builder, mappings, property, links)) { return; } // Property is a normal or non-managed property. - String propertyName = (null != propertyMapping ? propertyMapping.getPath() : persistentProperty.getName()); - Object propertyValue = wrapper.getProperty(persistentProperty); + Object propertyValue = wrapper.getProperty(property); try { - jgen.writeObjectField(propertyName, propertyValue); + jgen.writeObjectField(property.getName(), propertyValue); } catch(IOException e) { throw new IllegalStateException(e); } @@ -330,27 +297,22 @@ public class PersistentEntityJackson2Module extends SimpleModule implements Init }); // Add associations as links - persistentEntity.doWithAssociations(new AssociationHandler() { + entity.doWithAssociations(new AssociationHandler() { @Override public void doWithAssociation(Association association) { - PersistentProperty persistentProperty = association.getInverse(); - ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName()); - if(null != propertyMapping && !propertyMapping.isExported()) { + + PersistentProperty property = association.getInverse(); + + if(!mappings.isMapped(property)) { return; } - if(maybeAddAssociationLink(repositories, - config, - baseEntityUri, - repoInfo, - entityMapping, - propertyMapping, - persistentProperty, - links)) { + + if (maybeAddAssociationLink(builder, mappings, property, links)) { return; } // Association Link was not added, probably because this isn't a managed type. Add value of property inline. - Object propertyValue = wrapper.getProperty(persistentProperty); + Object propertyValue = wrapper.getProperty(property); try { - jgen.writeObjectField(persistentProperty.getName(), propertyValue); + jgen.writeObjectField(property.getName(), propertyValue); } catch(IOException e) { throw new IllegalStateException(e); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityToJsonSchemaConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java similarity index 75% rename from spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityToJsonSchemaConverter.java rename to spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java index 564ecf12c..cda9ebc1b 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/json/PersistentEntityToJsonSchemaConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java @@ -1,15 +1,13 @@ -package org.springframework.data.rest.repository.json; +package org.springframework.data.rest.webmvc.json; -import static org.springframework.data.rest.core.util.UriUtils.*; -import static org.springframework.data.rest.repository.json.PersistentEntityJackson2Module.*; -import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; +import static org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.*; import static org.springframework.util.StringUtils.*; -import java.net.URI; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; + import javax.annotation.Nonnull; import javax.validation.constraints.NotNull; @@ -21,10 +19,11 @@ import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PropertyHandler; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.rest.config.ResourceMapping; import org.springframework.data.rest.repository.annotation.Description; +import org.springframework.data.rest.repository.mapping.ResourceMappings; +import org.springframework.data.rest.repository.mapping.ResourceMetadata; import org.springframework.data.rest.repository.support.RepositoryInformationSupport; +import org.springframework.data.rest.webmvc.support.RepositoryLinkBuilder; import org.springframework.hateoas.Link; /** @@ -38,11 +37,15 @@ public class PersistentEntityToJsonSchemaConverter private static final TypeDescriptor STRING_TYPE = TypeDescriptor.valueOf(String.class); private static final TypeDescriptor SCHEMA_TYPE = TypeDescriptor.valueOf(JsonSchema.class); private Set convertiblePairs = new HashSet(); + private ResourceMappings mappings; @Override public void afterPropertiesSet() throws Exception { - for(Class domainType : repositories) { + + for(Class domainType : repositories) { convertiblePairs.add(new ConvertiblePair(domainType, JsonSchema.class)); } + + this.mappings = new ResourceMappings(config, repositories); } @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { @@ -60,15 +63,13 @@ public class PersistentEntityToJsonSchemaConverter @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + PersistentEntity persistentEntity = repositories.getPersistentEntity((Class)source); - final RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(persistentEntity.getType()); - final ResourceMapping repoMapping = getResourceMapping(config, repoInfo); - final ResourceMapping entityMapping = getResourceMapping(config, persistentEntity); - final URI baseEntityUri = buildUri(config.getBaseUri(), repoMapping.getPath(), "{id}"); + final ResourceMetadata metadata = mappings.getMappingFor(persistentEntity.getClass()); String entityDesc = persistentEntity.getType().isAnnotationPresent(Description.class) ? ((Description)persistentEntity.getType().getAnnotation(Description.class)).value() : null; - + final JsonSchema jsonSchema = new JsonSchema(persistentEntity.getName(), entityDesc); persistentEntity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(PersistentProperty persistentProperty) { @@ -95,23 +96,19 @@ public class PersistentEntityToJsonSchemaConverter }); final List links = new ArrayList(); + persistentEntity.doWithAssociations(new AssociationHandler() { @Override public void doWithAssociation(Association association) { PersistentProperty persistentProperty = association.getInverse(); - ResourceMapping propertyMapping = entityMapping.getResourceMappingFor(persistentProperty.getName()); - if(null != propertyMapping && !propertyMapping.isExported()) { + if(!metadata.isMapped(persistentProperty)) { return; } - maybeAddAssociationLink(repositories, - config, - baseEntityUri, - repoInfo, - entityMapping, - propertyMapping, - persistentProperty, - links); + + RepositoryLinkBuilder builder = new RepositoryLinkBuilder(metadata, config.getBaseUri()).slash("{id}"); + maybeAddAssociationLink(builder, mappings, persistentProperty, links); } }); + jsonSchema.add(links); return jsonSchema; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/package-info.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/package-info.java new file mode 100644 index 000000000..2533be269 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +/** + * + * @author Oliver Gierke + */ +package org.springframework.data.rest.webmvc.json; \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index fa3927120..ec997b7d0 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -1,131 +1,83 @@ package org.springframework.data.rest.webmvc.support; -import static org.springframework.data.rest.core.util.UriUtils.*; -import static org.springframework.data.rest.repository.support.ResourceMappingUtils.*; - -import java.net.URI; - import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -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.webmvc.RepositoryController; -import org.springframework.hateoas.Identifiable; +import org.springframework.data.rest.repository.mapping.ResourceMappings; +import org.springframework.data.rest.repository.mapping.ResourceMetadata; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkBuilder; import org.springframework.hateoas.core.AbstractEntityLinks; -import org.springframework.hateoas.mvc.ControllerLinkBuilder; -import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.util.Assert; /** * @author Jon Brisbin + * @author Oliver Gierke */ public class RepositoryEntityLinks extends AbstractEntityLinks { - private final Repositories repositories; + private final Repositories repositories; + private final ResourceMappings mappings; private final RepositoryRestConfiguration config; @Autowired - public RepositoryEntityLinks(Repositories repositories, - RepositoryRestConfiguration config) { + public RepositoryEntityLinks(Repositories repositories, ResourceMappings mappings, RepositoryRestConfiguration config) { + + Assert.notNull(repositories, "Repositories must not be null!"); + this.repositories = repositories; + this.mappings = mappings; this.config = config; } - @Override public boolean supports(Class delimiter) { - PersistentEntity persistentEntity = repositories.getPersistentEntity(delimiter); - return (null != persistentEntity); + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Class delimiter) { + return repositories.hasRepositoryFor(delimiter); } - @Override public LinkBuilder linkFor(Class type) { - RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type); - if(null == repoInfo) { - throw new IllegalArgumentException(type + " is not managed by any repository."); - } - PersistentEntity persistentEntity = repositories.getPersistentEntity(type); - if(null == persistentEntity) { - throw new IllegalArgumentException(type + " is not managed by any repository."); - } - return new PersistentEntityLinkBuilder(config.getBaseUri(), repoInfo, persistentEntity); + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkFor(java.lang.Class) + */ + @Override + public LinkBuilder linkFor(Class type) { + + ResourceMetadata metadata = mappings.getMappingFor(type); + return new RepositoryLinkBuilder(metadata, config.getBaseUri()); } - @Override public LinkBuilder linkFor(Class type, Object... parameters) { + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkFor(java.lang.Class, java.lang.Object[]) + */ + @Override + public LinkBuilder linkFor(Class type, Object... parameters) { return linkFor(type); } - @Override public Link linkToCollectionResource(Class type) { - RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type); - if(null == repoInfo) { - throw new IllegalArgumentException(type + " is not managed by any repository."); - } - ResourceMapping mapping = getResourceMapping(config, repoInfo); - return linkFor(type).withRel(mapping.getRel()); - } - - @Override public Link linkToSingleResource(Class type, Object id) { - RepositoryInformation repoInfo = repositories.getRepositoryInformationFor(type); - if(null == repoInfo) { - throw new IllegalArgumentException(type + " is not managed by any repository."); - } - ResourceMapping repoMapping = getResourceMapping(config, repoInfo); - PersistentEntity persistentEntity = repositories.getPersistentEntity(type); - ResourceMapping entityMapping = getResourceMapping(config, persistentEntity); - return linkFor(type).slash(id).withRel(repoMapping.getRel() + "." + entityMapping.getRel()); - } - - private class PersistentEntityLinkBuilder implements LinkBuilder { + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkToCollectionResource(java.lang.Class) + */ + @Override + public Link linkToCollectionResource(Class type) { - private final UriComponentsBuilder builder; - private final ResourceMapping repoMapping; - private final ResourceMapping entityMapping; - - private PersistentEntityLinkBuilder(URI baseUri, - RepositoryInformation repoInfo, - PersistentEntity persistentEntity) { - this.repoMapping = getResourceMapping(config, repoInfo); - this.entityMapping = getResourceMapping(config, persistentEntity); - if(null == baseUri) { - URI u = ControllerLinkBuilder.linkTo(RepositoryController.class).toUri(); - if(u.toString().endsWith("/")) { - String s = u.toString(); - baseUri = URI.create(s.substring(0, s.lastIndexOf('/'))); - } else { - baseUri = u; - } - } - this.builder = UriComponentsBuilder.fromUri(buildUri(baseUri, repoMapping.getPath())); - } - - @Override public LinkBuilder slash(Object object) { - String path = String.format("%s", object); - if(object instanceof PersistentProperty) { - String propName = ((PersistentProperty) object).getName(); - if(entityMapping.hasResourceMappingFor(propName)) { - path = entityMapping.getResourceMappingFor(propName).getPath(); - } - } - builder.pathSegment(path); - return this; - } - - @Override public LinkBuilder slash(Identifiable identifiable) { - return slash(identifiable.getId()); - } - - @Override public URI toUri() { - return builder.build().toUri(); - } - - @Override public Link withRel(String rel) { - return new Link(builder.build().toUriString(), rel); - } - - @Override public Link withSelfRel() { - return withRel("self"); - } + ResourceMetadata metadata = mappings.getMappingFor(type); + return linkFor(type).withRel(metadata.getRel()); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.EntityLinks#linkToSingleResource(java.lang.Class, java.lang.Object) + */ + @Override + public Link linkToSingleResource(Class type, Object id) { + + ResourceMetadata metadata = mappings.getMappingFor(type); + return linkFor(type).slash(id).withRel(metadata.getSingleResourceRel()); + } } 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 new file mode 100644 index 000000000..92b32132a --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryLinkBuilder.java @@ -0,0 +1,90 @@ +/* + * 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.webmvc.support; + +import java.net.URI; + +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.rest.repository.mapping.ResourceMetadata; +import org.springframework.data.rest.webmvc.RepositoryController; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.core.LinkBuilderSupport; +import org.springframework.hateoas.mvc.ControllerLinkBuilder; +import org.springframework.web.util.UriComponentsBuilder; + +public class RepositoryLinkBuilder extends LinkBuilderSupport { + + private final ResourceMetadata metadata; + + public RepositoryLinkBuilder(ResourceMetadata metadata, URI baseUri) { + this(metadata, prepareBuilder(baseUri, metadata)); + } + + private RepositoryLinkBuilder(ResourceMetadata metadata, UriComponentsBuilder builder) { + + super(builder); + this.metadata = metadata; + } + + private static UriComponentsBuilder prepareBuilder(URI baseUri, ResourceMetadata metadata) { + + UriComponentsBuilder builder = baseUri != null ? UriComponentsBuilder.fromUri(baseUri) : ControllerLinkBuilder.linkTo(RepositoryController.class).toUriComponentsBuilder(); + return builder.path(metadata.getPath()); + } + + @Override + public RepositoryLinkBuilder slash(Object object) { + + if (object instanceof PersistentProperty) { + return slash((PersistentProperty) object); + } + + return super.slash(object); + } + + public RepositoryLinkBuilder slash(PersistentProperty property) { + + String propName = property.getName(); + + if (metadata.isManaged(property)) { + return slash(metadata.getMappingFor(property).getPath()); + } else { + return slash(propName); + } + } + + public Link withResourceRel() { + return withRel(metadata.getRel()); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.LinkBuilderSupport#createNewInstance(org.springframework.web.util.UriComponentsBuilder) + */ + @Override + protected RepositoryLinkBuilder createNewInstance(UriComponentsBuilder builder) { + return new RepositoryLinkBuilder(this.metadata, builder); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.core.LinkBuilderSupport#getThis() + */ + @Override + protected RepositoryLinkBuilder getThis() { + return this; + } +} \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java new file mode 100644 index 000000000..534a5f3b5 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AbstractWebIntegrationTests.java @@ -0,0 +1,92 @@ +/* + * 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.webmvc; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.Before; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.core.DefaultLinkDiscoverer; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +/** + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@Transactional +@ContextConfiguration(classes = RepositoryRestMvcConfiguration.class) +public abstract class AbstractWebIntegrationTests { + + @Autowired WebApplicationContext context; + + protected MockMvc mvc; + LinkDiscoverer links = new DefaultLinkDiscoverer(); + + @Before + public void setUp() { + mvc = MockMvcBuilders.webAppContextSetup(context).build(); + } + + protected MockHttpServletResponse request(String href, MediaType contentType) throws Exception { + return mvc.perform(get(href).accept(contentType)). // + andExpect(status().isOk()). // + andExpect(content().contentType(MediaType.APPLICATION_JSON)). // + andReturn().getResponse(); + } + + protected MockHttpServletResponse request(Link link) throws Exception { + return request(link.getHref()); + } + + protected MockHttpServletResponse request(String href) throws Exception { + return request(href, MediaType.APPLICATION_JSON); + } + + protected Link assertHasLinkWithRel(String rel, MockHttpServletResponse response) throws Exception { + + String content = response.getContentAsString(); + Link link = links.findLinkWithRel(rel, content); + + assertThat("Expected to find link with rel " + rel + " but found none in " + content + "!", link, + is(notNullValue())); + + return link; + } + + protected void assertDoesNotHaveLinkWithRel(String rel, MockHttpServletResponse response) throws Exception { + + String content = response.getContentAsString(); + Link link = links.findLinkWithRel(rel, content); + + assertThat("Expected not to find link with rel " + rel + " but found " + link + "!", link, is(nullValue())); + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/CustomMethodArgumentResolverTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/CustomMethodArgumentResolverTests.java index 553733860..fc3cc3a57 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/CustomMethodArgumentResolverTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/CustomMethodArgumentResolverTests.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012-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.webmvc; import static org.hamcrest.MatcherAssert.*; @@ -7,18 +22,25 @@ import java.net.URI; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; import org.springframework.core.MethodParameter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.rest.config.RepositoryRestConfiguration; -import org.springframework.data.rest.repository.PagingAndSorting; import org.springframework.data.rest.webmvc.annotation.BaseURI; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; /** * @author Jon Brisbin + * @author Oliver Gierke */ -public class CustomMethodArgumentResolverTests extends AbstractJMockTests { +@RunWith(MockitoJUnitRunner.class) +public class CustomMethodArgumentResolverTests { static final MethodParameter BASE_URI; static final MethodParameter PAGE_SORT; @@ -30,7 +52,7 @@ public class CustomMethodArgumentResolverTests extends AbstractJMockTests { 0 ); PAGE_SORT = MethodParameter.forMethodOrConstructor( - Methods.class.getDeclaredMethod("pagingAndSorting", PagingAndSorting.class), + Methods.class.getDeclaredMethod("pagingAndSorting", Pageable.class), 0 ); } catch(NoSuchMethodException e) { @@ -43,15 +65,17 @@ public class CustomMethodArgumentResolverTests extends AbstractJMockTests { .setBaseUri(URI.create("http://localhost:8080")); private final BaseUriMethodArgumentResolver baseUriResolver = new BaseUriMethodArgumentResolver(config); - private final PagingAndSortingMethodArgumentResolver pageSortResolver = - new PagingAndSortingMethodArgumentResolver(config); + private final PageableHandlerMethodArgumentResolver pageSortResolver = + new PageableHandlerMethodArgumentResolver(); private ModelAndViewContainer mavContainer; - private WebDataBinderFactory webDataBinderFactory; + @Mock WebDataBinderFactory webDataBinderFactory; @Before public void setup() { mavContainer = new ModelAndViewContainer(); - webDataBinderFactory = context.mock(WebDataBinderFactory.class); + + pageSortResolver.setOneIndexedParameters(true); + pageSortResolver.setFallbackPageable(new PageRequest(1, 5)); } @Test @@ -80,7 +104,7 @@ public class CustomMethodArgumentResolverTests extends AbstractJMockTests { is(true)); // Resolve Page and Sort information - PagingAndSorting pageSort = (PagingAndSorting)pageSortResolver.resolveArgument( + Pageable pageSort = pageSortResolver.resolveArgument( PAGE_SORT, mavContainer, new ServletWebRequest(Requests.PAGE_REQUEST), @@ -92,15 +116,14 @@ public class CustomMethodArgumentResolverTests extends AbstractJMockTests { is(1)); assertThat("Finds limit parameter value", pageSort.getPageSize(), - is(5)); + is(10)); } static class Methods { void baseUri(@BaseURI URI baseUri) { } - void pagingAndSorting(PagingAndSorting pageSort) { + void pagingAndSorting(Pageable pageSort) { } } - } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/Requests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/Requests.java index 229d3eac7..e2890c11c 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/Requests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/Requests.java @@ -12,7 +12,7 @@ public abstract class Requests { static { PAGE_REQUEST.setParameter("page", "2"); - PAGE_REQUEST.setParameter("limit", "5"); + PAGE_REQUEST.setParameter("size", "10"); } private Requests() { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java index 8b37b3441..34725784c 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/ResourceProcessorHandlerMethodReturnValueHandlerUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-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. @@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import static org.springframework.data.rest.webmvc.HttpEntityMatcher.*; import static org.springframework.util.ReflectionUtils.*; @@ -28,13 +29,18 @@ import java.util.List; import java.util.Map; import org.hamcrest.Matcher; -import org.jmock.Expectations; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; import org.springframework.core.MethodParameter; +import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceProcessor; import org.springframework.hateoas.Resources; +import org.springframework.hateoas.mvc.HeaderLinksResponseEntity; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -48,7 +54,8 @@ import org.springframework.web.method.support.ModelAndViewContainer; * @author Oliver Gierke * @author Jon Brisbin */ -public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests extends AbstractJMockTests { +@RunWith(MockitoJUnitRunner.class) +public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests { static final Resource FOO = new Resource("foo"); static final Resources> FOOS = new Resources>( @@ -90,12 +97,11 @@ public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests extends A }); } - HandlerMethodReturnValueHandler delegate; + @Mock HandlerMethodReturnValueHandler delegate; List> resourceProcessors; @Before public void setUp() { - delegate = context.mock(HandlerMethodReturnValueHandler.class); resourceProcessors = new ArrayList>(); } @@ -188,6 +194,20 @@ public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests extends A invokeReturnValueHandler("resourceEntity", is(LONG_20), LONG_10_RES); } + + @Test + public void usesHeaderLinksResponseEntityIfConfigured() throws Exception { + + Resource resource = new Resource("foo", new Link("href", "rel")); + MethodParameter parameter = METHOD_PARAMS.get("resource"); + + ResourceProcessorHandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler(delegate, resourceProcessors); + handler.setRootLinksAsHeaders(true); + handler.handleReturnValue(resource, parameter, null, null); + + verify(delegate, times(1)).handleReturnValue(Mockito.any(HeaderLinksResponseEntity.class), eq(parameter), + Mockito.any(ModelAndViewContainer.class), Mockito.any(NativeWebRequest.class)); + } // Helpers ---------------------------------------------------------// private void invokeReturnValueHandler(String method, @@ -195,12 +215,9 @@ public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests extends A Object returnValue) throws Exception { final MethodParameter methodParam = METHOD_PARAMS.get(method); - context.checking(new Expectations() {{ - oneOf(delegate).handleReturnValue(with(matcher), - with(methodParam), - with(aNull(ModelAndViewContainer.class)), - with(aNull(NativeWebRequest.class))); - }}); + if (methodParam == null) { + throw new IllegalArgumentException("Invalid method!"); + } HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler( delegate, @@ -209,13 +226,10 @@ public class ResourceProcessorHandlerMethodReturnValueHandlerUnitTests extends A handler.handleReturnValue(returnValue, methodParam, null, null); } - private void assertSupport(final boolean value) { - final MethodParameter parameter = context.mock(MethodParameter.class); + private void assertSupport(boolean value) { - context.checking(new Expectations() {{ - oneOf(delegate).supportsReturnType(parameter); - will(returnValue(value)); - }}); + final MethodParameter parameter = Mockito.mock(MethodParameter.class); + when(delegate.supportsReturnType(Mockito.any(MethodParameter.class))).thenReturn(value); HandlerMethodReturnValueHandler handler = new ResourceProcessorHandlerMethodReturnValueHandler( delegate, diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java new file mode 100644 index 000000000..adc3591d0 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvConfigurationIntegrationTests.java @@ -0,0 +1,60 @@ +/* + * 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.webmvc.config; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.hateoas.core.DefaultRelProvider; + +/** + * @author Oliver Gierke + */ +public class RepositoryRestMvConfigurationIntegrationTests { + + static AbstractApplicationContext context; + + @BeforeClass + public static void setUp() { + context = new AnnotationConfigApplicationContext(ExtendingConfiguration.class); + } + + @AfterClass + public static void tearDown() { + if (context != null) { + context.close(); + } + } + + @Test + public void foo() { + context.getBean(PageableHandlerMethodArgumentResolver.class); + } + + @Configuration + static class ExtendingConfiguration extends RepositoryRestMvcConfiguration { + + @Bean + public DefaultRelProvider relProvider() { + return new DefaultRelProvider(); + } + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java index 0a22b8ee4..6a0447263 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaRepositoryConfig.java @@ -1,6 +1,20 @@ +/* + * Copyright 2012-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.webmvc.jpa; -import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; import org.springframework.context.annotation.Bean; @@ -9,20 +23,19 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; -import org.springframework.orm.jpa.JpaDialect; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.Database; -import org.springframework.orm.jpa.vendor.HibernateJpaDialect; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author Jon Brisbin + * @author Oliver Gierke */ @Configuration -@ComponentScan(basePackageClasses = JpaRepositoryConfig.class) +@ComponentScan @EnableJpaRepositories @EnableTransactionManagement public class JpaRepositoryConfig { @@ -32,7 +45,7 @@ public class JpaRepositoryConfig { return builder.setType(EmbeddedDatabaseType.HSQL).build(); } - @Bean public EntityManagerFactory entityManagerFactory() { + @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); vendorAdapter.setDatabase(Database.HSQL); vendorAdapter.setGenerateDdl(true); @@ -41,20 +54,12 @@ public class JpaRepositoryConfig { factory.setJpaVendorAdapter(vendorAdapter); factory.setPackagesToScan(getClass().getPackage().getName()); factory.setDataSource(dataSource()); - factory.afterPropertiesSet(); - - return factory.getObject(); - } - - @Bean public JpaDialect jpaDialect() { - return new HibernateJpaDialect(); + + return factory; } @Bean public PlatformTransactionManager transactionManager() { - JpaTransactionManager txManager = new JpaTransactionManager(); - txManager.setEntityManagerFactory(entityManagerFactory()); - return txManager; + return new JpaTransactionManager(); } - } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java new file mode 100644 index 000000000..10f64de2d --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/jpa/JpaWebTests.java @@ -0,0 +1,48 @@ +/* + * 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.webmvc.jpa; + +import org.junit.Test; +import org.springframework.data.rest.webmvc.AbstractWebIntegrationTests; +import org.springframework.hateoas.Link; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ContextConfiguration; + +/** + * Web integration tests specific to JPA. + * + * @author Oliver Gierke + */ +@ContextConfiguration(classes = JpaRepositoryConfig.class) +public class JpaWebTests extends AbstractWebIntegrationTests { + + @Test + public void accessPersons() throws Exception { + + MockHttpServletResponse response = request("/people?page=0&size=1"); + + Link nextLink = assertHasLinkWithRel(Link.REL_NEXT, response); + assertDoesNotHaveLinkWithRel(Link.REL_PREVIOUS, response); + + response = request(nextLink); + assertHasLinkWithRel(Link.REL_PREVIOUS, response); + nextLink = assertHasLinkWithRel(Link.REL_NEXT, response); + + response = request(nextLink); + assertHasLinkWithRel(Link.REL_PREVIOUS, response); + assertDoesNotHaveLinkWithRel(Link.REL_NEXT, response); + } +} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/json/PersistentEntitySerializationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java similarity index 58% rename from spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/json/PersistentEntitySerializationTests.java rename to spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java index c0db7aba0..98c00715c 100644 --- a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/json/PersistentEntitySerializationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntitySerializationTests.java @@ -1,36 +1,37 @@ -package org.springframework.data.rest.repository.json; +package org.springframework.data.rest.webmvc.json; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.Matchers.*; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.net.URI; +import java.io.StringWriter; import java.util.Collections; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.jayway.jsonpath.JsonPath; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.repository.PersistentEntityResource; -import org.springframework.data.rest.repository.RepositoryTestsConfig; -import org.springframework.data.rest.repository.domain.jpa.Person; -import org.springframework.data.rest.repository.domain.jpa.PersonRepository; +import org.springframework.data.rest.webmvc.jpa.Person; +import org.springframework.data.rest.webmvc.jpa.PersonRepository; import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.core.DefaultLinkDiscoverer; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.util.UriTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Jon Brisbin */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = RepositoryTestsConfig.class) +@Transactional public class PersistentEntitySerializationTests { private static final String PERSON_JSON_IN = "{\"firstName\": \"John\",\"lastName\": \"Doe\"}"; @@ -38,22 +39,19 @@ public class PersistentEntitySerializationTests { @Autowired ObjectMapper mapper; @Autowired Repositories repositories; @Autowired PersonRepository people; - - public static Matcher isLinkWithHref(final String href) { - return new BaseMatcher() { - @Override public boolean matches(Object item) { - return (item instanceof Link && href.equals(((Link)item).getHref())); - } - - @Override public void describeTo(Description description) { - description.appendText(href); - } - }; + + LinkDiscoverer linkDiscoverer; + + @Before + public void setUp() { + linkDiscoverer = new DefaultLinkDiscoverer(); } @Test public void deserializesPersonEntity() throws IOException { + Person p = mapper.readValue(PERSON_JSON_IN, Person.class); + assertThat(p.getFirstName(), is("John")); assertThat(p.getLastName(), is("Doe")); assertThat(p.getSiblings(), is(Collections.EMPTY_LIST)); @@ -62,13 +60,18 @@ public class PersistentEntitySerializationTests { @Test public void serializesPersonEntity() throws IOException, InterruptedException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); PersistentEntity persistentEntity = repositories.getPersistentEntity(Person.class); Person person = people.save(new Person("John", "Doe")); - mapper.writeValue(out, PersistentEntityResource.wrap(persistentEntity, person, URI.create("http://localhost"))); - out.flush(); - String s = new String(out.toByteArray()); - - assertThat("Siblings Link looks correct", JsonPath.read(s, "$links[0].href").toString(), endsWith("/2/siblings")); + + StringWriter writer = new StringWriter(); + mapper.writeValue(writer, PersistentEntityResource.wrap(persistentEntity, person)); + + String s = writer.toString(); + + Link fatherLink = linkDiscoverer.findLinkWithRel("father", s); + assertThat(fatherLink.getHref(), endsWith(new UriTemplate("/{id}/father").expand(person.getId()).toString())); + + Link siblingLink = linkDiscoverer.findLinkWithRel("siblings", s); + assertThat(siblingLink.getHref(), endsWith(new UriTemplate("/{id}/siblings").expand(person.getId()).toString())); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java new file mode 100644 index 000000000..9844ce5bd --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -0,0 +1,82 @@ +package org.springframework.data.rest.webmvc.json; + +import java.net.URI; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +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.repository.UriDomainClassConverter; +import org.springframework.data.rest.repository.mapping.ResourceMappings; +import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; +import org.springframework.data.rest.webmvc.jpa.Person; +import org.springframework.data.rest.webmvc.jpa.PersonRepository; +import org.springframework.format.support.DefaultFormattingConversionService; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Jon Brisbin + */ +@Configuration +@Import({JpaRepositoryConfig.class}) +@SuppressWarnings("deprecation") +public class RepositoryTestsConfig { + + @Autowired + private ApplicationContext appCtx; + + @Bean public Repositories repositories() { + return new Repositories(appCtx); + } + + @Bean public RepositoryRestConfiguration config() { + RepositoryRestConfiguration config = new RepositoryRestConfiguration(); + + config.setResourceMappingForDomainType(Person.class) + .setRel("person"); + +// config.setResourceMappingForRepository(ConfiguredPersonRepository.class) +// .setRel("people") +// .setPath("people") +// .setExported(false); + + config.setResourceMappingForRepository(PersonRepository.class) + .setRel("people") + .setPath("people") + .addResourceMappingFor("findByFirstName") + .setRel("firstname") + .setPath("firstname"); + + config.setBaseUri(URI.create("http://localhost:8080")); + + return config; + } + + @Bean public DefaultFormattingConversionService defaultConversionService() { + return new DefaultFormattingConversionService(); + } + + @Bean public DomainClassConverter domainClassConverter() { + return new DomainClassConverter(defaultConversionService()); + } + + @Bean public UriDomainClassConverter uriDomainClassConverter() { + return new UriDomainClassConverter(); + } + + @Bean public Module persistentEntityModule() { + return new PersistentEntityJackson2Module(new ResourceMappings(config(), repositories())); + } + + @Bean public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(persistentEntityModule()); + return mapper; + } +} diff --git a/spring-data-rest-webmvc/src/test/resources/logback.xml b/spring-data-rest-webmvc/src/test/resources/logback.xml new file mode 100644 index 000000000..a02b71a9a --- /dev/null +++ b/spring-data-rest-webmvc/src/test/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + \ No newline at end of file