DATAREST-1401 - RepositoryCollectionResourceMapping now rejects multi-segment path configurations.

Tweaked implementation of lookups to use Lazy to avoid repeated annotation inspections.
This commit is contained in:
Oliver Drotbohm
2019-06-26 16:21:05 +02:00
parent 49ee111504
commit 2d994c7076
2 changed files with 104 additions and 66 deletions

View File

@@ -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<LinkRelation> rel, itemResourceRel;
private final Lazy<ResourceDescription> description, itemDescription;
private final Lazy<Class<?>> 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<RestResource> annotation = Optional
.ofNullable(AnnotationUtils.findAnnotation(repositoryType, RestResource.class));
Optional<RepositoryRestResource> 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) //
.<ResourceDescription> map(it -> new AnnotationBasedResourceDescription(it, fallback)) //
.orElse(fallback);
});
this.itemDescription = Lazy.of(() -> {
ResourceDescription fallback = SimpleResourceDescription.defaultFor(getItemResourceRel());
return repositoryAnnotation.map(RepositoryRestResource::itemResourceDescription) //
.<ResourceDescription> 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<LinkRelation> toLinkRelation(Optional<String> source) {
Class<?> excerptProjection = repositoryAnnotation.excerptProjection();
return excerptProjection.equals(RepositoryRestResource.None.class) ? null : excerptProjection;
return source //
.filter(StringUtils::hasText) //
.map(LinkRelation::of);
}
}

View File

@@ -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<Person, Long> {}
@RepositoryRestResource(path = "first/second")
interface InvalidPath extends Repository<Person, Long> {}
@RepositoryRestResource(excerptProjection = Object.class)
interface WithProjection extends Repository<Person, Long> {}
interface WithoutProjection extends Repository<Person, Long> {}
}