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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user