From a50113afa9834d517c9605360b3d2b48cab1ff5f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tobias=20Wei=C3=9F?= <tweiss@meshcloud.io>
Date: Fri, 12 Jan 2018 16:35:02 +0100
Subject: [PATCH] DATAREST-1176 - Add explicitly method annotated detection
 strategy

---
 .../CrudMethodsSupportedHttpMethods.java      | 19 +++++++++++++------
 .../RepositoryAwareResourceMetadata.java      |  2 +-
 .../mapping/RepositoryDetectionStrategy.java  | 12 ++++++++++++
 .../RepositoryMethodResourceMapping.java      | 13 +++++++++++--
 .../mapping/RepositoryResourceMappings.java   |  8 +++++++-
 ...dMethodsSupportedHttpMethodsUnitTests.java |  8 ++++++--
 ...epositoryDetectionStrategiesUnitTests.java | 13 +++++++++++++
 ...ositoryMethodResourceMappingUnitTests.java |  3 ++-
 8 files changed, 65 insertions(+), 13 deletions(-)

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 d493ee651..62c400036 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
@@ -19,7 +19,6 @@
 import static org.springframework.http.HttpMethod.*;
 
 import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
 
 import java.lang.reflect.Method;
 import java.util.Collections;
@@ -49,11 +48,14 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods {
 	 *
 	 * @param crudMethods must not be {@literal null}.
 	 */
-	public CrudMethodsSupportedHttpMethods(CrudMethods crudMethods) {
+	public CrudMethodsSupportedHttpMethods(CrudMethods crudMethods, RepositoryResourceMappings provider) {
 
 		Assert.notNull(crudMethods, "CrudMethods must not be null!");
 
-		this.exposedMethods = new DefaultExposureAwareCrudMethods(crudMethods);
+		boolean exportedDefault = provider.getRepositoryDetectionStrategy()
+				!= RepositoryDetectionStrategy.RepositoryDetectionStrategies.EXPLICIT_METHOD_ANNOTATED;
+
+		this.exposedMethods = new DefaultExposureAwareCrudMethods(crudMethods, exportedDefault);
 	}
 
 	/*
@@ -139,10 +141,15 @@ public Set<HttpMethod> getMethodsFor(PersistentProperty<?> property) {
 	/**
 	 * @author Oliver Gierke
 	 */
-	@RequiredArgsConstructor
 	private static class DefaultExposureAwareCrudMethods implements ExposureAwareCrudMethods {
 
 		private final @NonNull CrudMethods crudMethods;
+		private final boolean exportedDefault;
+
+		DefaultExposureAwareCrudMethods(CrudMethods crudMethods, boolean exportedDefault) {
+			this.crudMethods = crudMethods;
+			this.exportedDefault = exportedDefault;
+		}
 
 		/*
 		 * (non-Javadoc)
@@ -180,12 +187,12 @@ public boolean exposesFindAll() {
 			return exposes(crudMethods.getFindAllMethod());
 		}
 
-		private static boolean exposes(Optional<Method> method) {
+		private boolean exposes(Optional<Method> method) {
 
 			return method.map(it -> {
 
 				RestResource annotation = AnnotationUtils.findAnnotation(it, RestResource.class);
-				return annotation == null ? true : annotation.exported();
+				return annotation == null ? exportedDefault : annotation.exported();
 
 			}).orElse(false);
 		}
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 ef7e42b60..4bb9c7d37 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,7 @@ public RepositoryAwareResourceMetadata(PersistentEntity<?, ?> entity, Collection
 		this.mapping = mapping;
 		this.provider = provider;
 		this.repositoryMetadata = repositoryMetadata;
-		this.crudMethodsSupportedHttpMethods = new CrudMethodsSupportedHttpMethods(repositoryMetadata.getCrudMethods());
+		this.crudMethodsSupportedHttpMethods = new CrudMethodsSupportedHttpMethods(repositoryMetadata.getCrudMethods(), provider);
 	}
 
 	/**
diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryDetectionStrategy.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryDetectionStrategy.java
index fe4199735..7209b1442 100644
--- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryDetectionStrategy.java
+++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/RepositoryDetectionStrategy.java
@@ -92,6 +92,18 @@ public boolean isExported(RepositoryMetadata metadata) {
 		 */
 		ANNOTATED {
 
+			@Override
+			public boolean isExported(RepositoryMetadata metadata) {
+				return isExplicitlyExported(metadata.getRepositoryInterface(), false);
+			}
+		},
+
+		/**
+		 * Behaves like the annotated strategy on repository level. But it does not export all methods
+		 * of an exported Repository. The methods have to be annotated explicitly too.
+		 */
+		EXPLICIT_METHOD_ANNOTATED {
+
 			@Override
 			public boolean isExported(RepositoryMetadata metadata) {
 				return isExplicitlyExported(metadata.getRepositoryInterface(), false);
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 f885702d8..9fdb368d5 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
@@ -62,7 +62,8 @@ class RepositoryMethodResourceMapping implements MethodResourceMapping {
 	 * @param method must not be {@literal null}.
 	 * @param resourceMapping must not be {@literal null}.
 	 */
-	public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMapping, RepositoryMetadata metadata) {
+	public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMapping, RepositoryMetadata metadata,
+										   RepositoryDetectionStrategy strategy) {
 
 		Assert.notNull(method, "Method must not be null!");
 		Assert.notNull(resourceMapping, "ResourceMapping must not be null!");
@@ -70,7 +71,7 @@ public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMa
 		RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class);
 		String resourceRel = resourceMapping.getRel();
 
-		this.isExported = annotation != null ? annotation.exported() : true;
+		this.isExported = determineIsExported(strategy, annotation);
 		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());
@@ -84,6 +85,14 @@ public RepositoryMethodResourceMapping(Method method, ResourceMapping resourceMa
 		this.metadata = metadata;
 	}
 
+	private boolean determineIsExported(RepositoryDetectionStrategy strategy, RestResource annotation) {
+
+		boolean exportedDefault = strategy
+				!= RepositoryDetectionStrategy.RepositoryDetectionStrategies.EXPLICIT_METHOD_ANNOTATED;
+
+		return annotation != null ? annotation.exported() : exportedDefault;
+	}
+
 	private static final List<ParameterMetadata> discoverParameterMetadata(Method method, String baseRel) {
 
 		List<ParameterMetadata> result = new ArrayList<ParameterMetadata>();
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 5128ed99d..1ceb3123c 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
@@ -40,6 +40,7 @@
 public class RepositoryResourceMappings extends PersistentEntitiesResourceMappings {
 
 	private final Repositories repositories;
+	private final RepositoryDetectionStrategy strategy;
 	private final Map<Class<?>, SearchResourceMappings> searchCache = new HashMap<Class<?>, SearchResourceMappings>();
 
 	/**
@@ -73,6 +74,7 @@ public RepositoryResourceMappings(Repositories repositories, PersistentEntities
 		Assert.notNull(strategy, "RepositoryDetectionStrategy must not be null!");
 
 		this.repositories = repositories;
+		this.strategy = strategy;
 		this.populateCache(repositories, relProvider, strategy);
 	}
 
@@ -118,7 +120,7 @@ public SearchResourceMappings getSearchResourceMappings(Class<?> domainType) {
 		if (resourceMapping.isExported()) {
 			for (Method queryMethod : repositoryInformation.getQueryMethods()) {
 				RepositoryMethodResourceMapping methodMapping = new RepositoryMethodResourceMapping(queryMethod,
-						resourceMapping, repositoryInformation);
+						resourceMapping, repositoryInformation, strategy);
 				if (methodMapping.isExported()) {
 					mappings.add(methodMapping);
 				}
@@ -156,4 +158,8 @@ public boolean hasMappingFor(Class<?> type) {
 	public boolean isMapped(PersistentProperty<?> property) {
 		return repositories.hasRepositoryFor(property.getActualType()) && super.isMapped(property);
 	}
+
+	public RepositoryDetectionStrategy getRepositoryDetectionStrategy() {
+		return strategy;
+	}
 }
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 d66f22f29..a7f9c637a 100755
--- 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
@@ -25,6 +25,7 @@
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 import org.springframework.data.annotation.ReadOnlyProperty;
 import org.springframework.data.annotation.Reference;
@@ -47,6 +48,9 @@
 @RunWith(MockitoJUnitRunner.class)
 public class CrudMethodsSupportedHttpMethodsUnitTests {
 
+	@Mock
+	private RepositoryResourceMappings mappings;
+
 	@Test // DATACMNS-589, DATAREST-409
 	public void doesNotSupportAnyHttpMethodForEmptyRepository() {
 
@@ -118,12 +122,12 @@ public void doesNotSupportDeleteIfNoFindOneAvailable() {
 		assertMethodsSupported(getSupportedHttpMethodsFor(NoFindOne.class), ITEM, false, DELETE);
 	}
 
-	private static SupportedHttpMethods getSupportedHttpMethodsFor(Class<?> repositoryInterface) {
+	private SupportedHttpMethods getSupportedHttpMethodsFor(Class<?> repositoryInterface) {
 
 		RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
 		CrudMethods crudMethods = new DefaultCrudMethods(metadata);
 
-		return new CrudMethodsSupportedHttpMethods(crudMethods);
+		return new CrudMethodsSupportedHttpMethods(crudMethods, mappings);
 	}
 
 	private static void assertMethodsSupported(SupportedHttpMethods methods, ResourceType type, boolean supported,
diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryDetectionStrategiesUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryDetectionStrategiesUnitTests.java
index 7c656c018..b270b3134 100755
--- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryDetectionStrategiesUnitTests.java
+++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/RepositoryDetectionStrategiesUnitTests.java
@@ -89,6 +89,19 @@ public void annotatedHonorsAnnotationsOnly() {
 		});
 	}
 
+	@Test // DATAREST-1176
+	public void onlyExplicitAnnotatedMethodsAreExposed() {
+
+		assertExposures(EXPLICIT_METHOD_ANNOTATED, new HashMap<Class<?>, Boolean>() {
+			{
+				put(AnnotatedRepository.class, true);
+				put(HiddenRepository.class, false);
+				put(PublicRepository.class, false);
+				put(PackageProtectedRepository.class, false);
+			}
+		});
+	}
+
 	private static void assertExposures(RepositoryDetectionStrategy strategy, Map<Class<?>, Boolean> expected) {
 
 		for (Entry<Class<?>, Boolean> entry : expected.entrySet()) {
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 5c85ce12f..425579469 100755
--- 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
@@ -131,7 +131,8 @@ public void doesNotIncludePageableAsParameter() throws Exception {
 	}
 
 	private RepositoryMethodResourceMapping getMappingFor(Method method) {
-		return new RepositoryMethodResourceMapping(method, resourceMapping, metadata);
+		RepositoryDetectionStrategy strategy = RepositoryDetectionStrategies.DEFAULT;
+		return new RepositoryMethodResourceMapping(method, resourceMapping, metadata, strategy);
 	}
 
 	static class Person {}