DATAREST-1176 - Repository method exposure can now be controlled via RepositoryRestConfiguration.

The default exposure of repository methods is now controlled via RepositoryRestConfiguration.setExposeRepositoryMethodsByDefault(…). That allows us to remove the additional API from RepositoryDetectionStrategy as the mere detection is an orthogonal topic.

Also 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:
Oliver Gierke
2018-01-16 17:33:34 +01:00
parent 6b61d2b026
commit a10aa934b7
9 changed files with 70 additions and 83 deletions

View File

@@ -25,6 +25,8 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import org.springframework.data.repository.support.Repositories; 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;
import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy.RepositoryDetectionStrategies; import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy.RepositoryDetectionStrategies;
import org.springframework.data.rest.core.support.EntityLookup; import org.springframework.data.rest.core.support.EntityLookup;
@@ -65,6 +67,7 @@ public class RepositoryRestConfiguration {
private ResourceMappingConfiguration domainMappings = new ResourceMappingConfiguration(); private ResourceMappingConfiguration domainMappings = new ResourceMappingConfiguration();
private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration(); private ResourceMappingConfiguration repoMappings = new ResourceMappingConfiguration();
private RepositoryDetectionStrategy repositoryDetectionStrategy = RepositoryDetectionStrategies.DEFAULT; private RepositoryDetectionStrategy repositoryDetectionStrategy = RepositoryDetectionStrategies.DEFAULT;
private boolean exposeRepositoryMethodsByDefault = true;
/** /**
* The {@link RelProvider} to be used to calculate the link relation defaults for repositories. * The {@link RelProvider} to be used to calculate the link relation defaults for repositories.
@@ -571,6 +574,47 @@ public class RepositoryRestConfiguration {
return this; return this;
} }
/**
* Returns whether to expose repository methods by default.
*
* @since 3.1
* @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 3.1
* @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 3.1
* @see #setRepositoryDetectionStrategy(RepositoryDetectionStrategy)
* @see #setExposeRepositoryMethodsByDefault(boolean)
*/
public void disableDefaultExposure() {
setRepositoryDetectionStrategy(RepositoryDetectionStrategies.ANNOTATED);
setExposeRepositoryMethodsByDefault(false);
}
/** /**
* Returns the {@link RepositoryCorsRegistry} to configure Cross-origin resource sharing. * Returns the {@link RepositoryCorsRegistry} to configure Cross-origin resource sharing.
* *

View File

@@ -40,17 +40,6 @@ public interface RepositoryDetectionStrategy {
*/ */
boolean isExported(RepositoryMetadata metadata); boolean isExported(RepositoryMetadata metadata);
/**
* Returns whether to expose repository methods by default, i.e. without the need to explicitly annotate them with
* {@link RestResource}.
*
* @return
* @since 3.1
*/
default boolean exposeMethodsByDefault() {
return true;
}
/** /**
* A variety of strategies to determine repository exposure. * A variety of strategies to determine repository exposure.
* *
@@ -108,30 +97,6 @@ public interface RepositoryDetectionStrategy {
public boolean isExported(RepositoryMetadata metadata) { public boolean isExported(RepositoryMetadata metadata) {
return isExplicitlyExported(metadata.getRepositoryInterface(), false); return isExplicitlyExported(metadata.getRepositoryInterface(), false);
} }
},
/**
* Behaves like the {@link RepositoryDetectionStrategies#ANNOTATED} strategy on repository level, but only exports
* the methods of the repository that have been explicitly annotated with {@link RestResource}. CRUD methods need to
* be annotated, too, for the default collection resource exposure to be applied.
*
* @since 3.1
*/
EXPLICITLY_ANNOTATED {
@Override
public boolean isExported(RepositoryMetadata metadata) {
return isExplicitlyExported(metadata.getRepositoryInterface(), false);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy#exposeMethodsByDefault()
*/
@Override
public boolean exposeMethodsByDefault() {
return false;
}
}; };
/** /**

View File

@@ -29,7 +29,6 @@ import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.annotation.RestResource; import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@@ -41,22 +40,9 @@ import org.springframework.util.Assert;
public class RepositoryResourceMappings extends PersistentEntitiesResourceMappings { public class RepositoryResourceMappings extends PersistentEntitiesResourceMappings {
private final Repositories repositories; private final Repositories repositories;
private final RepositoryDetectionStrategy strategy; private final RepositoryRestConfiguration configuration;
private final Map<Class<?>, SearchResourceMappings> searchCache = new HashMap<Class<?>, SearchResourceMappings>(); private final Map<Class<?>, SearchResourceMappings> searchCache = new HashMap<Class<?>, SearchResourceMappings>();
/**
* Creates a new {@link RepositoryResourceMappings} using the given {@link Repositories} and
* {@link PersistentEntities}.
*
* @param repositories must not be {@literal null}.
* @param entities must not be {@literal null}.
* @param strategy 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}, * Creates a new {@link RepositoryResourceMappings} from the given {@link RepositoryRestConfiguration},
* {@link Repositories} and {@link RelProvider}. * {@link Repositories} and {@link RelProvider}.
@@ -67,20 +53,19 @@ public class RepositoryResourceMappings extends PersistentEntitiesResourceMappin
* @param relProvider must not be {@literal null}. * @param relProvider must not be {@literal null}.
*/ */
public RepositoryResourceMappings(Repositories repositories, PersistentEntities entities, public RepositoryResourceMappings(Repositories repositories, PersistentEntities entities,
RepositoryDetectionStrategy strategy, RelProvider relProvider) { RepositoryRestConfiguration configuration) {
super(entities); super(entities);
Assert.notNull(repositories, "Repositories must not be null!"); 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.repositories = repositories;
this.strategy = strategy; this.configuration = configuration;
this.populateCache(repositories, relProvider, strategy); this.populateCache(repositories, configuration);
} }
private final void populateCache(Repositories repositories, RelProvider provider, private final void populateCache(Repositories repositories, RepositoryRestConfiguration configuration) {
RepositoryDetectionStrategy strategy) {
for (Class<?> type : repositories) { for (Class<?> type : repositories) {
@@ -88,6 +73,9 @@ public class RepositoryResourceMappings extends PersistentEntitiesResourceMappin
Class<?> repositoryInterface = repositoryInformation.getRepositoryInterface(); Class<?> repositoryInterface = repositoryInformation.getRepositoryInterface();
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(type); PersistentEntity<?, ?> entity = repositories.getPersistentEntity(type);
RepositoryDetectionStrategy strategy = configuration.getRepositoryDetectionStrategy();
RelProvider provider = configuration.getRelProvider();
CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(repositoryInformation, strategy, CollectionResourceMapping mapping = new RepositoryCollectionResourceMapping(repositoryInformation, strategy,
provider); provider);
RepositoryAwareResourceMetadata information = new RepositoryAwareResourceMetadata(entity, mapping, this, RepositoryAwareResourceMetadata information = new RepositoryAwareResourceMetadata(entity, mapping, this,
@@ -165,9 +153,9 @@ public class RepositoryResourceMappings extends PersistentEntitiesResourceMappin
* {@link RestResource}. * {@link RestResource}.
* *
* @since 3.1 * @since 3.1
* @see RepositoryDetectionStrategy#exposeMethodsByDefault() * @see RepositoryRestConfiguration#exposeRepositoryMethodsByDefault()
*/ */
public boolean exposeMethodsByDefault() { public boolean exposeMethodsByDefault() {
return strategy.exposeMethodsByDefault(); return configuration.exposeRepositoryMethodsByDefault();
} }
} }

View File

@@ -89,21 +89,6 @@ public class RepositoryDetectionStrategiesUnitTests {
}); });
} }
@Test // DATAREST-1176
public void onlyExplicitAnnotatedMethodsAreExposed() {
assertExposures(EXPLICITLY_ANNOTATED, new HashMap<Class<?>, Boolean>() {
{
put(AnnotatedRepository.class, true);
put(HiddenRepository.class, false);
put(PublicRepository.class, false);
put(PackageProtectedRepository.class, false);
}
});
assertThat(EXPLICITLY_ANNOTATED.exposeMethodsByDefault()).isFalse();
}
private static void assertExposures(RepositoryDetectionStrategy strategy, Map<Class<?>, Boolean> expected) { private static void assertExposures(RepositoryDetectionStrategy strategy, Map<Class<?>, Boolean> expected) {
for (Entry<Class<?>, Boolean> entry : expected.entrySet()) { for (Entry<Class<?>, Boolean> entry : expected.entrySet()) {

View File

@@ -38,9 +38,9 @@ import org.springframework.data.rest.core.mapping.RepositoryDetectionStrategy.Re
*/ */
public class RepositoryMethodResourceMappingUnitTests { public class RepositoryMethodResourceMappingUnitTests {
RepositoryDetectionStrategy strategy = RepositoryDetectionStrategies.DEFAULT;
RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class);
RepositoryCollectionResourceMapping resourceMapping = new RepositoryCollectionResourceMapping(metadata, strategy); RepositoryCollectionResourceMapping resourceMapping = new RepositoryCollectionResourceMapping(metadata,
RepositoryDetectionStrategies.DEFAULT);
@Test @Test
public void defaultsMappingToMethodName() throws Exception { public void defaultsMappingToMethodName() throws Exception {
@@ -131,7 +131,7 @@ public class RepositoryMethodResourceMappingUnitTests {
} }
private RepositoryMethodResourceMapping getMappingFor(Method method) { private RepositoryMethodResourceMapping getMappingFor(Method method) {
return new RepositoryMethodResourceMapping(method, resourceMapping, metadata, strategy.exposeMethodsByDefault()); return new RepositoryMethodResourceMapping(method, resourceMapping, metadata, true);
} }
static class Person {} static class Person {}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.rest.core.mapping; package org.springframework.data.rest.core.mapping;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -32,13 +33,15 @@ import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.Path; 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.Author;
import org.springframework.data.rest.core.domain.CreditCard; import org.springframework.data.rest.core.domain.CreditCard;
import org.springframework.data.rest.core.domain.JpaRepositoryConfig; import org.springframework.data.rest.core.domain.JpaRepositoryConfig;
import org.springframework.data.rest.core.domain.Person; import org.springframework.data.rest.core.domain.Person;
import org.springframework.data.rest.core.domain.Profile; import org.springframework.data.rest.core.domain.Profile;
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.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@@ -62,9 +65,12 @@ public class RepositoryResourceMappingsIntegrationTests {
mappingContext.getPersistentEntity(Profile.class); mappingContext.getPersistentEntity(Profile.class);
RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(),
new MetadataConfiguration(), mock(EnumTranslationConfiguration.class));
Repositories repositories = new Repositories(factory); Repositories repositories = new Repositories(factory);
this.mappings = new RepositoryResourceMappings(repositories, new PersistentEntities(Arrays.asList(mappingContext)), this.mappings = new RepositoryResourceMappings(repositories, new PersistentEntities(Arrays.asList(mappingContext)),
RepositoryDetectionStrategies.DEFAULT, new EvoInflectorRelProvider()); configuration);
} }
@Test @Test

View File

@@ -107,7 +107,7 @@ public class RepositoryTestsConfig {
public Module persistentEntityModule() { public Module persistentEntityModule() {
RepositoryResourceMappings mappings = new RepositoryResourceMappings(repositories(), persistentEntities(), RepositoryResourceMappings mappings = new RepositoryResourceMappings(repositories(), persistentEntities(),
config().getRepositoryDetectionStrategy()); config());
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(), EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
mock(PagingAndSortingTemplateVariables.class), mock(PagingAndSortingTemplateVariables.class),
Java8PluginRegistry.of(Arrays.asList(DefaultIdConverter.INSTANCE))); Java8PluginRegistry.of(Arrays.asList(DefaultIdConverter.INSTANCE)));

View File

@@ -115,7 +115,7 @@ public class RepositoryTestsConfig {
public Module persistentEntityModule() { public Module persistentEntityModule() {
RepositoryResourceMappings mappings = new RepositoryResourceMappings(repositories(), persistentEntities(), RepositoryResourceMappings mappings = new RepositoryResourceMappings(repositories(), persistentEntities(),
config().getRepositoryDetectionStrategy()); config());
EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(), EntityLinks entityLinks = new RepositoryEntityLinks(repositories(), mappings, config(),
mock(PagingAndSortingTemplateVariables.class), mock(PagingAndSortingTemplateVariables.class),
Java8PluginRegistry.of(Arrays.asList(DefaultIdConverter.INSTANCE))); Java8PluginRegistry.of(Arrays.asList(DefaultIdConverter.INSTANCE)));

View File

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