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:
committed by
Oliver Gierke
parent
ce17416e16
commit
f3a0e111e6
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user