+ * Exported Repository classes can be annotated with {@link CrossOrigin} to configure CORS for a specific repository.
+ *
+ * @author Mark Paluch
+ * @since 2.6
+ */
+ static class CorsConfigurationAccessor {
+
+ private final ResourceMappings mappings;
+ private final Repositories repositories;
+ private final StringValueResolver embeddedValueResolver;
+
+ /**
+ * Creates a new {@link CorsConfigurationAccessor} given {@link ResourceMappings}, {@link Repositories} and
+ * {@link StringValueResolver}.
+ *
+ * @param mappings must not be {@literal null}.
+ * @param repositories must not be {@literal null}.
+ * @param embeddedValueResolver may be {@literal null} if not present.
+ */
+ CorsConfigurationAccessor(ResourceMappings mappings, Repositories repositories,
+ StringValueResolver embeddedValueResolver) {
+
+ Assert.notNull(mappings, "ResourceMappings must not be null!");
+ Assert.notNull(repositories, "Repositories must not be null!");
+
+ this.mappings = mappings;
+ this.repositories = repositories;
+ this.embeddedValueResolver = embeddedValueResolver;
+ }
+
+ CorsConfiguration findCorsConfiguration(String lookupPath) {
+
+ ResourceMetadata resource = getResourceMetadata(getRepositoryBasePath(lookupPath));
+
+ return resource != null ? createConfiguration(
+ repositories.getRepositoryInformationFor(resource.getDomainType()).getRepositoryInterface()) : null;
+ }
+
+ private ResourceMetadata getResourceMetadata(String basePath) {
+
+ if (mappings.exportsTopLevelResourceFor(basePath)) {
+
+ for (ResourceMetadata metadata : mappings) {
+ if (metadata.getPath().matches(basePath) && metadata.isExported()) {
+ return metadata;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates {@link CorsConfiguration} from a repository interface.
+ *
+ * @param repositoryInterface the repository interface
+ * @return {@link CorsConfiguration} or {@literal null}.
+ */
+ protected CorsConfiguration createConfiguration(Class> repositoryInterface) {
+
+ CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(repositoryInterface, CrossOrigin.class);
+
+ if (typeAnnotation == null) {
+ return null;
+ }
+
+ CorsConfiguration config = new CorsConfiguration();
+ updateCorsConfig(config, typeAnnotation);
+
+ if (CollectionUtils.isEmpty(config.getAllowedOrigins())) {
+ config.setAllowedOrigins(Arrays.asList(CrossOrigin.DEFAULT_ORIGINS));
+ }
+
+ if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
+ for (HttpMethod httpMethod : HttpMethod.values()) {
+ config.addAllowedMethod(httpMethod);
+ }
+ }
+
+ if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
+ config.setAllowedHeaders(Arrays.asList(CrossOrigin.DEFAULT_ALLOWED_HEADERS));
+ }
+
+ if (config.getAllowCredentials() == null) {
+ config.setAllowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS);
+ }
+
+ if (config.getMaxAge() == null) {
+ config.setMaxAge(CrossOrigin.DEFAULT_MAX_AGE);
+ }
+
+ return config;
+ }
+
+ private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) {
+
+ for (String origin : annotation.origins()) {
+ config.addAllowedOrigin(resolveCorsAnnotationValue(origin));
+ }
+
+ for (RequestMethod method : annotation.methods()) {
+ config.addAllowedMethod(method.name());
+ }
+
+ for (String header : annotation.allowedHeaders()) {
+ config.addAllowedHeader(resolveCorsAnnotationValue(header));
+ }
+
+ for (String header : annotation.exposedHeaders()) {
+ config.addExposedHeader(resolveCorsAnnotationValue(header));
+ }
+
+ String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials());
+
+ if ("true".equalsIgnoreCase(allowCredentials)) {
+ config.setAllowCredentials(true);
+ } else if ("false".equalsIgnoreCase(allowCredentials)) {
+ config.setAllowCredentials(false);
+ } else if (!allowCredentials.isEmpty()) {
+ throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", "
+ + "or an empty string (\"\"): current value is [" + allowCredentials + "]");
+ }
+
+ if (annotation.maxAge() >= 0 && config.getMaxAge() == null) {
+ config.setMaxAge(annotation.maxAge());
+ }
+ }
+
+ private String resolveCorsAnnotationValue(String value) {
+ return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value);
+ }
+ }
}
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 d861ddbdc..28a8955ca 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
@@ -21,6 +21,7 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.BeanCreationException;
@@ -139,9 +140,11 @@
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
+import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@@ -570,6 +573,16 @@ public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
return handlerAdapter;
}
+ /**
+ * {@link HttpRequestHandlerAdapter} to handle CORS preflight requests.
+ *
+ * @return
+ */
+ @Bean
+ public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
+ return new HttpRequestHandlerAdapter();
+ }
+
/**
* The {@link HandlerMapping} to delegate requests to Spring Data REST controllers. Sets up a
* {@link DelegatingHandlerMapping} to make sure manually implemented {@link BasePathAwareController} instances that
@@ -582,13 +595,18 @@ public RequestMappingHandlerAdapter repositoryExporterHandlerAdapter() {
@Bean
public DelegatingHandlerMapping restHandlerMapping() {
- RepositoryRestHandlerMapping repositoryMapping = new RepositoryRestHandlerMapping(resourceMappings(), config());
+ Map