diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java index aa0f4fde2..2d3f099de 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java @@ -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. * diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CrudMethodsSupportedHttpMethods.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CrudMethodsSupportedHttpMethods.java index fcdf637e6..506ac6415 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CrudMethodsSupportedHttpMethods.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/CrudMethodsSupportedHttpMethods.java @@ -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(); } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceMetadata.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceMetadata.java index 14fffa29e..a852382ed 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceMetadata.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryAwareResourceMetadata.java @@ -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()); } /** diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java index dd931149c..74ad5ce20 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMapping.java @@ -42,7 +42,6 @@ import org.springframework.util.StringUtils; */ class RepositoryMethodResourceMapping implements MethodResourceMapping { - @SuppressWarnings("unchecked") // private static final Collection> 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()); diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java index 034353875..0ec9de68e 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappings.java @@ -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, SearchResourceMappings> searchCache = new HashMap, 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(); + } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/CrudMethodsSupportedHttpMethodsUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/CrudMethodsSupportedHttpMethodsUnitTests.java index ea022b671..7cc01fdfa 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/CrudMethodsSupportedHttpMethodsUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/CrudMethodsSupportedHttpMethodsUnitTests.java @@ -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 {} + interface MethodsExplicitlyExportedRepository extends Repository { + + @RestResource + S save(S entity); + + @RestResource + void delete(Object entity); + } + class Entity { Entity embedded; diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java index e6a6105b5..d697173d8 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryMethodResourceMappingUnitTests.java @@ -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 {} diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappingsIntegrationTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappingsIntegrationTests.java index 0baf960d4..a91e3a747 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappingsIntegrationTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryResourceMappingsIntegrationTests.java @@ -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 diff --git a/spring-data-rest-tests/spring-data-rest-tests-core/src/test/java/org/springframework/data/rest/tests/RepositoryTestsConfig.java b/spring-data-rest-tests/spring-data-rest-tests-core/src/test/java/org/springframework/data/rest/tests/RepositoryTestsConfig.java index d26397033..fb60b5ddc 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-core/src/test/java/org/springframework/data/rest/tests/RepositoryTestsConfig.java +++ b/spring-data-rest-tests/spring-data-rest-tests-core/src/test/java/org/springframework/data/rest/tests/RepositoryTestsConfig.java @@ -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., BackendIdConverter> create(Arrays.asList(DefaultIdConverter.INSTANCE))); diff --git a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java index f430462fc..dae95964e 100644 --- a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java +++ b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -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., BackendIdConverter> create(Arrays.asList(DefaultIdConverter.INSTANCE))); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 514f50ea9..710e7a8d0 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -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()); } /**