diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java index 5204e03f9..ef62df407 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMapping.java @@ -15,6 +15,8 @@ */ package org.springframework.data.rest.core.mapping; +import java.util.Optional; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotationUtils; @@ -22,6 +24,8 @@ import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.rest.core.Path; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.Optionals; import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.server.LinkRelationProvider; import org.springframework.hateoas.server.core.EvoInflectorLinkRelationProvider; @@ -41,11 +45,13 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryCollectionResourceMapping.class); private static final boolean EVO_INFLECTOR_IS_PRESENT = ClassUtils.isPresent("org.atteo.evo.inflector.English", null); - private final RestResource annotation; - private final RepositoryRestResource repositoryAnnotation; - private final CollectionResourceMapping domainTypeMapping; private final boolean repositoryExported; private final RepositoryMetadata metadata; + private final Path path; + + private final Lazy rel, itemResourceRel; + private final Lazy description, itemDescription; + private final Lazy> excerptProjection; public RepositoryCollectionResourceMapping(RepositoryMetadata metadata, RepositoryDetectionStrategy strategy) { this(metadata, strategy, new EvoInflectorLinkRelationProvider()); @@ -68,17 +74,70 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { Class repositoryType = metadata.getRepositoryInterface(); + Optional annotation = Optional + .ofNullable(AnnotationUtils.findAnnotation(repositoryType, RestResource.class)); + Optional repositoryAnnotation = Optional + .ofNullable(AnnotationUtils.findAnnotation(repositoryType, RepositoryRestResource.class)); + this.metadata = metadata; - this.annotation = AnnotationUtils.findAnnotation(repositoryType, RestResource.class); - this.repositoryAnnotation = AnnotationUtils.findAnnotation(repositoryType, RepositoryRestResource.class); this.repositoryExported = strategy.isExported(metadata); Class domainType = metadata.getDomainType(); - this.domainTypeMapping = EVO_INFLECTOR_IS_PRESENT + ? new EvoInflectorTypeBasedCollectionResourceMapping(domainType, relProvider) : new TypeBasedCollectionResourceMapping(domainType, relProvider); - if (annotation != null) { + this.rel = Lazy.of(() -> Optionals.firstNonEmpty(// + () -> toLinkRelation(repositoryAnnotation.map(RepositoryRestResource::collectionResourceRel)), // + () -> toLinkRelation(annotation.map(RestResource::rel))) // + .orElseGet(domainTypeMapping::getRel)); + + this.itemResourceRel = Lazy + .of(() -> toLinkRelation(repositoryAnnotation.map(RepositoryRestResource::itemResourceRel)) // + .orElseGet(domainTypeMapping::getItemResourceRel)); + + this.path = Optionals.firstNonEmpty(// + () -> repositoryAnnotation.map(RepositoryRestResource::path), // + () -> annotation.map(RestResource::path)) // + .filter(StringUtils::hasText) // + .filter(it -> { + + if (it.contains("/")) { + throw new IllegalStateException( + String.format("Path %s configured for %s must only contain a single path segment!", it, + metadata.getRepositoryInterface().getName())); + } + + return true; + }) + // .peek(it -> it.wait()) // + .map(Path::new)// + .orElseGet(domainTypeMapping::getPath); + + this.description = Lazy.of(() -> { + + ResourceDescription fallback = SimpleResourceDescription.defaultFor(getRel()); + + return repositoryAnnotation.map(RepositoryRestResource::collectionResourceDescription) // + . map(it -> new AnnotationBasedResourceDescription(it, fallback)) // + .orElse(fallback); + }); + + this.itemDescription = Lazy.of(() -> { + + ResourceDescription fallback = SimpleResourceDescription.defaultFor(getItemResourceRel()); + + return repositoryAnnotation.map(RepositoryRestResource::itemResourceDescription) // + . map(it -> new AnnotationBasedResourceDescription(it, fallback)) // + .orElse(fallback); + }); + + this.excerptProjection = Lazy.of(() -> repositoryAnnotation// + .map(RepositoryRestResource::excerptProjection)// + .filter(it -> !it.equals(RepositoryRestResource.None.class)) // + .orElse(null)); + + if (annotation.isPresent()) { LOGGER.warn( "@RestResource detected to customize the repository resource for {}! Use @RepositoryRestResource instead!", metadata.getRepositoryInterface().getName()); @@ -91,20 +150,7 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { */ @Override public Path getPath() { - - Path fallback = domainTypeMapping.getPath(); - - if (repositoryAnnotation != null) { - String path = repositoryAnnotation.path(); - return StringUtils.hasText(path) ? new Path(path) : fallback; - } - - if (annotation != null) { - String path = annotation.path(); - return StringUtils.hasText(path) ? new Path(path) : fallback; - } - - return fallback; + return path; } /* @@ -113,20 +159,7 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { */ @Override public LinkRelation getRel() { - - LinkRelation fallback = domainTypeMapping.getRel(); - - if (repositoryAnnotation != null) { - String rel = repositoryAnnotation.collectionResourceRel(); - return StringUtils.hasText(rel) ? LinkRelation.of(rel) : fallback; - } - - if (annotation != null) { - String rel = annotation.rel(); - return StringUtils.hasText(rel) ? LinkRelation.of(rel) : fallback; - } - - return fallback; + return rel.get(); } /* @@ -135,15 +168,7 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { */ @Override public LinkRelation getItemResourceRel() { - - LinkRelation fallback = domainTypeMapping.getItemResourceRel(); - - if (repositoryAnnotation != null) { - String rel = repositoryAnnotation.itemResourceRel(); - return StringUtils.hasText(rel) ? LinkRelation.of(rel) : fallback; - } - - return fallback; + return itemResourceRel.get(); } /* @@ -170,14 +195,7 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { */ @Override public ResourceDescription getDescription() { - - ResourceDescription fallback = SimpleResourceDescription.defaultFor(getRel()); - - if (repositoryAnnotation != null) { - return new AnnotationBasedResourceDescription(repositoryAnnotation.collectionResourceDescription(), fallback); - } - - return fallback; + return description.get(); } /* @@ -186,14 +204,7 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { */ @Override public ResourceDescription getItemResourceDescription() { - - ResourceDescription fallback = SimpleResourceDescription.defaultFor(getItemResourceRel()); - - if (repositoryAnnotation != null) { - return new AnnotationBasedResourceDescription(repositoryAnnotation.itemResourceDescription(), fallback); - } - - return fallback; + return itemDescription.get(); } /* @@ -202,13 +213,13 @@ class RepositoryCollectionResourceMapping implements CollectionResourceMapping { */ @Override public Class getExcerptProjection() { + return excerptProjection.getOptional().orElse(null); + } - if (repositoryAnnotation == null) { - return null; - } + private static Optional toLinkRelation(Optional source) { - Class excerptProjection = repositoryAnnotation.excerptProjection(); - - return excerptProjection.equals(RepositoryRestResource.None.class) ? null : excerptProjection; + return source // + .filter(StringUtils::hasText) // + .map(LinkRelation::of); } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java index 07aa03a62..f10ce35b5 100755 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryCollectionResourceMappingUnitTests.java @@ -106,6 +106,25 @@ public class RepositoryCollectionResourceMappingUnitTests { assertThat(mapping.getPath()).isEqualTo(new Path("/objects")); } + @Test // DATAREST-1401 + public void rejectsPathsWithMultipleSegments() { + + RepositoryMetadata metadata = new DefaultRepositoryMetadata(InvalidPath.class); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> new RepositoryCollectionResourceMapping(metadata, RepositoryDetectionStrategies.DEFAULT)) // + .withMessageContaining("first/second") // + .withMessageContaining(InvalidPath.class.getName()) // + .withMessageContaining("path segment"); + } + + @Test // DATAREST-1401 + public void exposesProjectionTypeIfConfigured() { + + assertThat(getResourceMappingFor(WithProjection.class).getExcerptProjection()).isEqualTo(Object.class); + assertThat(getResourceMappingFor(WithoutProjection.class).getExcerptProjection()).isNull(); + } + private static CollectionResourceMapping getResourceMappingFor(Class repositoryInterface) { RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface); @@ -133,4 +152,12 @@ public class RepositoryCollectionResourceMappingUnitTests { @RepositoryRestResource(collectionResourceRel = "foo", itemResourceRel = "bar") interface RepositoryAnnotatedRepository extends Repository {} + + @RepositoryRestResource(path = "first/second") + interface InvalidPath extends Repository {} + + @RepositoryRestResource(excerptProjection = Object.class) + interface WithProjection extends Repository {} + + interface WithoutProjection extends Repository {} }