DATAREST-1176 - RepositoryRestConfiguration now allows to disable the default exposure.

The default exposure of repository methods is now controlled via RepositoryRestConfiguration.setExposeRepositoryMethodsByDefault(…). If set to true (the default), a repository that is detected for exposure (which in turn can be controlled via RepositoryDetectionStrategy) will have default resources exposed in case of the mere presence of CRUD methods. Setting this to false will require you to explicitly annotated those methods with @RestResource.

Added RepositoryRestConfiguration.disableDefaultExposure() to set the RepositoryDetectionStategy to ANNOTATED and disables default method exposure in one go. That can be exposed via a Spring Boot configuration property downstream.
This commit is contained in:
Tobias Weiß
2018-01-12 16:35:02 +01:00
committed by Oliver Gierke
parent ce17416e16
commit f3a0e111e6
11 changed files with 129 additions and 42 deletions

View File

@@ -25,6 +25,8 @@ import java.util.Collections;
import java.util.List;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy;
import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy.RepositoryDetectionStrategies;
import org.springframework.data.rest.core.support.EntityLookup;
@@ -65,6 +67,7 @@ public class RepositoryRestConfiguration {
private ResourceMappingConfiguration domainMappings = new ResourceMappingConfiguration();
private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration();
private RepositoryDetectionStrategy repositoryDetectionStrategy = RepositoryDetectionStrategies.DEFAULT;
private boolean exposeRepositoryMethodsByDefault = true;
/**
* The {@link RelProvider} to be used to calculate the link relation defaults for repositories.
@@ -572,6 +575,47 @@ public class RepositoryRestConfiguration {
return this;
}
/**
* Returns whether to expose repository methods by default.
*
* @since 2.6.10
* @see #setExposeRepositoryMethodsByDefault(boolean)
*/
public boolean exposeRepositoryMethodsByDefault() {
return this.exposeRepositoryMethodsByDefault;
}
/**
* Sets whether to expose repository methods by default. If this is disabled, CRUD methods must be annotated with
* {@link RestResource} explicitly to expose the default set of resources (opt-in). If this is set to {@literal true}
* (default), repository methods methods are exposed unless explictly annotated with {@link RestResource} and
* {@link RestResource#exported()} set to {@literal false}.
*
* @since 2.6.10
* @see #setRepositoryDetectionStrategy(RepositoryDetectionStrategy)
*/
public void setExposeRepositoryMethodsByDefault(boolean exposeRepositoryMethodsByDefault) {
this.exposeRepositoryMethodsByDefault = exposeRepositoryMethodsByDefault;
}
/**
* Disables the default exposure of repositories entirely. I.e. repositories to be exported must now be explicitly
* annotated with {@link RepositoryRestResource} and methods need to be annotated with {@link RestResource} to trigger
* exposure of default resources. Basically a shortcut for calling both
* {@link #setRepositoryDetectionStrategy(RepositoryDetectionStrategy)} to
* {@link RepositoryDetectionStrategies#ANNOTATED} and setting {@link #setExposeRepositoryMethodsByDefault(boolean)}
* to {@literal false}.
*
* @since 2.6.10
* @see #setRepositoryDetectionStrategy(RepositoryDetectionStrategy)
* @see #setExposeRepositoryMethodsByDefault(boolean)
*/
public void disableDefaultExposure() {
setRepositoryDetectionStrategy(RepositoryDetectionStrategies.ANNOTATED);
setExposeRepositoryMethodsByDefault(false);
}
/**
* Returns the {@link RepositoryCorsRegistry} to configure Cross-origin resource sharing.
*

View File

@@ -47,12 +47,14 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods {
* Creates a new {@link CrudMethodsSupportedHttpMethods} for the given {@link CrudMethods}.
*
* @param crudMethods must not be {@literal null}.
* @param methodsExposedByDefault whether repository methods should be considered exposed by default or need to be
* annotated with {@link RestResource} to really be visible.
*/
public CrudMethodsSupportedHttpMethods(CrudMethods crudMethods) {
public CrudMethodsSupportedHttpMethods(CrudMethods crudMethods, boolean methodsExposedByDefault) {
Assert.notNull(crudMethods, "CrudMethods must not be null!");
this.exposedMethods = new DefaultExposureAwareCrudMethods(crudMethods, methodsExposedByDefault);
this.exposedMethods = new DefaultExposureAwareCrudMethods(crudMethods);
}
/*
@@ -142,6 +144,7 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods {
private static class DefaultExposureAwareCrudMethods implements ExposureAwareCrudMethods {
private final @NonNull CrudMethods crudMethods;
private final boolean exportedDefault;
/*
* (non-Javadoc)
@@ -179,14 +182,14 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods {
return exposes(crudMethods.getFindAllMethod());
}
private static boolean exposes(Method method) {
private boolean exposes(Method method) {
if (method == null) {
return false;
}
RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class);
return annotation == null ? true : annotation.exported();
return annotation == null ? exportedDefault : annotation.exported();
}
}

View File

@@ -57,7 +57,8 @@ class RepositoryAwareResourceMetadata implements ResourceMetadata {
this.mapping = mapping;
this.provider = provider;
this.repositoryMetadata = repositoryMetadata;
this.crudMethodsSupportedHttpMethods = new CrudMethodsSupportedHttpMethods(repositoryMetadata.getCrudMethods());
this.crudMethodsSupportedHttpMethods = new CrudMethodsSupportedHttpMethods(repositoryMetadata.getCrudMethods(),
provider.exposeMethodsByDefault());
}
/**

View File

@@ -42,7 +42,6 @@ import org.springframework.util.StringUtils;
*/
class RepositoryMethodResourceMapping implements MethodResourceMapping {
@SuppressWarnings("unchecked") //
private static final Collection<Class<?>> IMPLICIT_PARAMETER_TYPES = Arrays.asList(Pageable.class, Sort.class);
private static final AnnotationAttribute PARAM_VALUE = new AnnotationAttribute(Param.class);
@@ -61,8 +60,11 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping {
*
* @param method must not be {@literal null}.
* @param resourceMapping must not be {@literal null}.
* @param metadata can be {@literal null}.
* @param whether the methods are supposed to be exported by default.
*/
public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMapping, RepositoryMetadata metadata) {
public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMapping, RepositoryMetadata metadata,
boolean exposeMethodsByDefault) {
Assert.notNull(method, "Method must not be null!");
Assert.notNull(resourceMapping, "ResourceMapping must not be null!");
@@ -70,7 +72,7 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping {
RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class);
String resourceRel = resourceMapping.getRel();
this.isExported = annotation != null ? annotation.exported() : true;
this.isExported = annotation != null ? annotation.exported() : exposeMethodsByDefault;
this.rel = annotation == null || !StringUtils.hasText(annotation.rel()) ? method.getName() : annotation.rel();
this.path = annotation == null || !StringUtils.hasText(annotation.path()) ? new Path(method.getName())
: new Path(annotation.path());

View File

@@ -26,9 +26,9 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.util.Assert;
/**
@@ -40,50 +40,40 @@ import org.springframework.util.Assert;
public class RepositoryResourceMappings extends PersistentEntitiesResourceMappings {
private final Repositories repositories;
private final RepositoryRestConfiguration configuration;
private final Map<Class<?>, SearchResourceMappings> searchCache = new HashMap<Class<?>, SearchResourceMappings>();
/**
* Creates a new {@link RepositoryResourceMappings} using the given {@link Repositories} and
* {@link PersistentEntities}.
* Creates a new {@link RepositoryResourceMappings} from the given {@link RepositoryRestConfiguration},
* {@link Repositories} and {@link RepositoryRestConfiguration}.
*
* @param repositories must not be {@literal null}.
* @param entities must not be {@literal null}.
* @param strategy must not be {@literal null}.
* @param configuration must not be {@literal null}.
*/
public RepositoryResourceMappings(Repositories repositories, PersistentEntities entities,
RepositoryDetectionStrategy strategy) {
this(repositories, entities, strategy, new EvoInflectorRelProvider());
}
/**
* Creates a new {@link RepositoryResourceMappings} from the given {@link RepositoryRestConfiguration},
* {@link Repositories} and {@link RelProvider}.
*
* @param repositories must not be {@literal null}.
* @param entities must not be {@literal null}.
* @param strategy must not be {@literal null}.
* @param relProvider must not be {@literal null}.
*/
public RepositoryResourceMappings(Repositories repositories, PersistentEntities entities, RepositoryDetectionStrategy strategy,
RelProvider relProvider) {
RepositoryRestConfiguration configuration) {
super(entities);
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(strategy, "RepositoryDetectionStrategy must not be null!");
Assert.notNull(configuration, "RepositoryRestConfiguration must not be null!");
this.repositories = repositories;
this.populateCache(repositories, relProvider, strategy);
this.configuration = configuration;
this.populateCache(repositories, configuration);
}
private final void populateCache(Repositories repositories, RelProvider provider,
RepositoryDetectionStrategy strategy) {
private final void populateCache(Repositories repositories, RepositoryRestConfiguration configuration) {
for (Class<?> type : repositories) {
RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(type);
Class<?> repositoryInterface = repositoryInformation.getRepositoryInterface();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(type);
RepositoryDetectionStrategy strategy = configuration.getRepositoryDetectionStrategy();
RelProvider provider = configuration.getRelProvider();
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(repositoryInformation, strategy,
provider);
@@ -118,7 +108,7 @@ public class RepositoryResourceMappings extends PersistentEntitiesResourceMappin
if (resourceMapping.isExported()) {
for (Method queryMethod : repositoryInformation.getQueryMethods()) {
RepositoryMethodResourceMapping methodMapping = new RepositoryMethodResourceMapping(queryMethod,
resourceMapping, repositoryInformation);
resourceMapping, repositoryInformation, exposeMethodsByDefault());
if (methodMapping.isExported()) {
mappings.add(methodMapping);
}
@@ -156,4 +146,15 @@ public class RepositoryResourceMappings extends PersistentEntitiesResourceMappin
public boolean isMapped(PersistentProperty<?> property) {
return repositories.hasRepositoryFor(property.getActualType()) && super.isMapped(property);
}
/**
* Returns whether to expose repository methods by default, i.e. without the need to explicitly annotate them with
* {@link RestResource}.
*
* @since 2.6.10
* @see RepositoryRestConfiguration#exposeRepositoryMethodsByDefault()
*/
public boolean exposeMethodsByDefault() {
return configuration.exposeRepositoryMethodsByDefault();
}
}

View File

@@ -17,14 +17,17 @@ package org.springframework.data.rest.core.mapping;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.rest.core.mapping.ResourceType.*;
import static org.springframework.http.HttpMethod.*;
import java.util.List;
import java.util.Set;
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.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Reference;
@@ -47,6 +50,13 @@ import org.springframework.http.HttpMethod;
@RunWith(MockitoJUnitRunner.class)
public class CrudMethodsSupportedHttpMethodsUnitTests {
@Mock RepositoryResourceMappings mappings;
@Before
public void setUp() {
when(mappings.exposeMethodsByDefault()).thenReturn(true);
}
@Test // DATACMNS-589, DATAREST-409
public void doesNotSupportAnyHttpMethodForEmptyRepository() {
@@ -116,12 +126,24 @@ public class CrudMethodsSupportedHttpMethodsUnitTests {
assertMethodsSupported(getSupportedHttpMethodsFor(NoFindOne.class), ITEM, false, DELETE);
}
private static SupportedHttpMethods getSupportedHttpMethodsFor(Class<?> repositoryInterface) {
@Test // DATAREST-1176
public void onlyExposesExplicitlyAnnotatedMethodsIfConfigured() {
reset(mappings);
when(mappings.exposeMethodsByDefault()).thenReturn(false);
assertMethodsSupported(getSupportedHttpMethodsFor(MethodsExplicitlyExportedRepository.class), COLLECTION, true,
POST, OPTIONS);
assertMethodsSupported(getSupportedHttpMethodsFor(MethodsExplicitlyExportedRepository.class), ITEM, true, OPTIONS,
PUT, PATCH);
}
private SupportedHttpMethods getSupportedHttpMethodsFor(Class<?> repositoryInterface) {
RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
CrudMethods crudMethods = new DefaultCrudMethods(metadata);
return new CrudMethodsSupportedHttpMethods(crudMethods);
return new CrudMethodsSupportedHttpMethods(crudMethods, mappings.exposeMethodsByDefault());
}
private static void assertMethodsSupported(SupportedHttpMethods methods, ResourceType type, boolean supported,
@@ -159,6 +181,15 @@ public class CrudMethodsSupportedHttpMethodsUnitTests {
interface EntityRepository extends CrudRepository<Entity, Long> {}
interface MethodsExplicitlyExportedRepository extends Repository<Object, Long> {
@RestResource
<S extends Object> S save(S entity);
@RestResource
void delete(Object entity);
}
class Entity {
Entity embedded;

View File

@@ -132,7 +132,7 @@ public class RepositoryMethodResourceMappingUnitTests {
}
private RepositoryMethodResourceMapping getMappingFor(Method method) {
return new RepositoryMethodResourceMapping(method, resourceMapping, metadata);
return new RepositoryMethodResourceMapping(method, resourceMapping, metadata, true);
}
static class Person {}

View File

@@ -17,6 +17,7 @@ package org.springframework.data.rest.core.mapping;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.Arrays;
@@ -34,12 +35,14 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.config.EnumTranslationConfiguration;
import org.springframework.data.rest.core.config.MetadataConfiguration;
import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.domain.Author;
import org.springframework.data.rest.core.domain.CreditCard;
import org.springframework.data.rest.core.domain.JpaRepositoryConfig;
import org.springframework.data.rest.core.domain.Person;
import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy.RepositoryDetectionStrategies;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -61,9 +64,12 @@ public class RepositoryResourceMappingsIntegrationTests {
@Before
public void setUp() {
RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(),
new MetadataConfiguration(), mock(EnumTranslationConfiguration.class));
Repositories repositories = new Repositories(factory);
this.mappings = new RepositoryResourceMappings(repositories, new PersistentEntities(Arrays.asList(mappingContext)),
RepositoryDetectionStrategies.DEFAULT, new EvoInflectorRelProvider());
configuration);
}
@Test

View File

@@ -108,7 +108,7 @@ public class RepositoryTestsConfig {
public Module persistentEntityModule() {
RepositoryResourceMappings mappings = new RepositoryResourceMappings(repositories(), persistentEntities(),
config().getRepositoryDetectionStrategy());
config());
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
mock(PagingAndSortingTemplateVariables.class),
OrderAwarePluginRegistry.<Class<?>, BackendIdConverter> create(Arrays.asList(DefaultIdConverter.INSTANCE)));

View File

@@ -116,7 +116,7 @@ public class RepositoryTestsConfig {
public Module persistentEntityModule() {
RepositoryResourceMappings mappings = new RepositoryResourceMappings(repositories(), persistentEntities(),
config().getRepositoryDetectionStrategy());
config());
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
mock(PagingAndSortingTemplateVariables.class),
OrderAwarePluginRegistry.<Class<?>, BackendIdConverter> create(Arrays.asList(DefaultIdConverter.INSTANCE)));

View File

@@ -615,8 +615,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon
@Bean
public RepositoryResourceMappings resourceMappings() {
return new RepositoryResourceMappings(repositories(), persistentEntities(),
config().getRepositoryDetectionStrategy(), config().getRelProvider());
return new RepositoryResourceMappings(repositories(), persistentEntities(), config());
}
/**