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 4840e97d3..5ee8aa9da 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 @@ -27,6 +27,7 @@ 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.ExposureConfiguration; 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; @@ -78,6 +79,7 @@ public class RepositoryRestConfiguration { private final ProjectionDefinitionConfiguration projectionConfiguration; private final MetadataConfiguration metadataConfiguration; private final EntityLookupConfiguration entityLookupConfiguration; + private final @Getter ExposureConfiguration exposureConfiguration; private final EnumTranslationConfiguration enumTranslationConfiguration; private boolean enableEnumTranslation = false; @@ -100,6 +102,7 @@ public class RepositoryRestConfiguration { this.metadataConfiguration = metadataConfiguration; this.enumTranslationConfiguration = enumTranslationConfiguration; this.entityLookupConfiguration = new EntityLookupConfiguration(); + this.exposureConfiguration = new ExposureConfiguration(); } /** diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ComposableFilter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ComposableFilter.java new file mode 100644 index 000000000..eac73853e --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ComposableFilter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +interface ComposableFilter { + + S filter(T first, S second); + + default ComposableFilter andThen(ComposableFilter filter) { + return (first, second) -> filter.filter(first, filter(first, second)); + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ConfigurableHttpMethods.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ConfigurableHttpMethods.java new file mode 100644 index 000000000..8b388a53c --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ConfigurableHttpMethods.java @@ -0,0 +1,131 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; + +/** + * {@link HttpMethods} that expose methods to create different {@link ConfigurableHttpMethods}. + * + * @author Oliver Gierke + * @since 3.1 + */ +@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PACKAGE) +public class ConfigurableHttpMethods implements HttpMethods { + + public static final ConfigurableHttpMethods NONE = ConfigurableHttpMethods.of(); + public static final ConfigurableHttpMethods ALL = ConfigurableHttpMethods.of(HttpMethod.values()); + + private final Collection methods; + + /** + * Creates a new {@link ConfigurableHttpMethods} of the given {@link HttpMethod}s. + * + * @param methods must not be {@literal null}. + * @return + */ + static ConfigurableHttpMethods of(HttpMethod... methods) { + + Assert.notNull(methods, "HttpMethods must not be null!"); + + return new ConfigurableHttpMethods(Arrays.stream(methods).collect(Collectors.toSet())); + } + + /** + * Creates a new {@link ConfigurableHttpMethods} of the given {@link HttpMethods}. + * + * @param methods must not be {@literal null}. + * @return + */ + static ConfigurableHttpMethods of(HttpMethods methods) { + + Assert.notNull(methods, "HttpMethods must not be null!"); + + if (ConfigurableHttpMethods.class.isInstance(methods)) { + return ConfigurableHttpMethods.class.cast(methods); + } + + return new ConfigurableHttpMethods(methods.stream().collect(Collectors.toSet())); + } + + /** + * Disables the given {@link HttpMethod}s. + * + * @param methods must not be {@literal null}. + * @return + */ + public ConfigurableHttpMethods disable(HttpMethod... methods) { + + Assert.notNull(methods, "HttpMethods must not be null!"); + + List toRemove = Arrays.asList(methods); + + return new ConfigurableHttpMethods(this.methods.stream() // + .filter(it -> !toRemove.contains(it)) // + .collect(Collectors.toSet())); + } + + /** + * Enables the given {@link HttpMethod}s. + * + * @param methods must not be {@literal null}. + * @return + */ + public HttpMethods enable(HttpMethod... methods) { + + Assert.notNull(methods, "HttpMethods must not be null!"); + + List toAdd = Arrays.asList(methods); + + if (this.methods.containsAll(toAdd)) { + return this; + } + + return ConfigurableHttpMethods.of(Stream.concat(this.methods.stream(), toAdd.stream()).collect(Collectors.toSet())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.HttpMethods#contains(org.springframework.http.HttpMethod) + */ + @Override + public boolean contains(HttpMethod method) { + + Assert.notNull(method, "HTTP method must not be null!"); + + return methods.contains(method); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return methods.iterator(); + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ConfigurationApplyingSupportedHttpMethodsAdapter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ConfigurationApplyingSupportedHttpMethodsAdapter.java new file mode 100644 index 000000000..187b6fea2 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ConfigurationApplyingSupportedHttpMethodsAdapter.java @@ -0,0 +1,77 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import org.springframework.data.mapping.PersistentProperty; + +/** + * Adapter for a {@link SupportedHttpMethods} instance that applies settings made through {@link ExposureConfiguration} + * to the calculated {@link ConfigurableHttpMethods} + * + * @author Oliver Gierke + * @see ExposureConfiguration + * @since 3.1 + */ +@RequiredArgsConstructor +public class ConfigurationApplyingSupportedHttpMethodsAdapter implements SupportedHttpMethods { + + private final @NonNull ExposureConfiguration configuration; + private final @NonNull ResourceMetadata resourceMetadata; + private final @NonNull SupportedHttpMethods delegate; + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#getMethodsFor(org.springframework.data.rest.core.mapping.ResourceType) + */ + @Override + public HttpMethods getMethodsFor(ResourceType type) { + + HttpMethods methods = delegate.getMethodsFor(type); + + return configuration.filter(ConfigurableHttpMethods.of(methods), type, resourceMetadata); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#getMethodsFor(org.springframework.data.mapping.PersistentProperty) + */ + @Override + public HttpMethods getMethodsFor(PersistentProperty property) { + + HttpMethods methodsFor = delegate.getMethodsFor(property); + ResourceMapping mapping = resourceMetadata.getMappingFor(property); + + if (!PropertyAwareResourceMapping.class.isInstance(mapping)) { + return methodsFor; + } + + ConfigurableHttpMethods methods = ConfigurableHttpMethods.of(methodsFor); + + return configuration.filter(methods, PropertyAwareResourceMapping.class.cast(mapping)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#allowsPutForCreation() + */ + @Override + public boolean allowsPutForCreation() { + return configuration.allowsPutForCreation(resourceMetadata); + } +} 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 1aa7b5424..34742a96a 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.data.rest.core.mapping.ResourceType.*; import static org.springframework.http.HttpMethod.*; import java.lang.reflect.Method; -import java.util.Collections; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -61,7 +60,7 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods { * @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#getSupportedHttpMethods(org.springframework.data.rest.core.mapping.ResourceType) */ @Override - public Set getMethodsFor(ResourceType resourceType) { + public HttpMethods getMethodsFor(ResourceType resourceType) { Assert.notNull(resourceType, "Resource type must not be null!"); @@ -105,7 +104,7 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods { throw new IllegalArgumentException(String.format("Unsupported resource type %s!", resourceType)); } - return Collections.unmodifiableSet(methods); + return HttpMethods.of(methods); } /* @@ -113,10 +112,10 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods { * @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#getMethodsFor(org.springframework.data.mapping.PersistentProperty) */ @Override - public Set getMethodsFor(PersistentProperty property) { + public HttpMethods getMethodsFor(PersistentProperty property) { if (!property.isAssociation()) { - return Collections.emptySet(); + return HttpMethods.none(); } Set methods = new HashSet(); @@ -133,7 +132,7 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods { methods.add(POST); } - return methods; + return HttpMethods.of(methods); } /** diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfiguration.java new file mode 100644 index 000000000..6b83603b1 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfiguration.java @@ -0,0 +1,233 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import static org.springframework.http.HttpMethod.*; + +import lombok.RequiredArgsConstructor; + +import java.util.function.Function; + +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; + +/** + * Configuration type to register filters customizing the HTTP methods supported. By default, filters registered are + * applied globally. Domain type specific filters can be registered via {@link #forDomainType(Class)}. Useful global + * shortcuts like {@link #disablePutOnItemResources()} do exist as well. + * + * @author Oliver Gierke + * @see #forDomainType(Class) + * @since 3.1 + */ +public class ExposureConfiguration implements ExposureConfigurer { + + private ComposableFilter collection = AggregateResourceHttpMethodsFilter + .none(); + private ComposableFilter item = AggregateResourceHttpMethodsFilter.none(); + private ComposableFilter property = AssociationResourceHttpMethodsFilter + .none(); + + private Function, Boolean> creationViaPut = __ -> true; + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ExposureConfigurer#withCollectionExposure(org.springframework.data.rest.core.mapping.ExposureConfigurer.AggregateResourceHttpMethodsFilter) + */ + @Override + public ExposureConfiguration withCollectionExposure(AggregateResourceHttpMethodsFilter filter) { + + this.collection = filter; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ExposureConfigurer#withItemExposure(org.springframework.data.rest.core.mapping.ExposureConfigurer.AggregateResourceHttpMethodsFilter) + */ + @Override + public ExposureConfiguration withItemExposure(AggregateResourceHttpMethodsFilter filter) { + + this.item = filter; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ExposureConfigurer#withAssociationExposure(org.springframework.data.rest.core.mapping.ExposureConfigurer.AssociationResourceHttpMethodsFilter) + */ + @Override + public ExposureConfiguration withAssociationExposure(AssociationResourceHttpMethodsFilter filter) { + + this.property = filter; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ExposureConfigurer#disablePutForCreation() + */ + @Override + public ExposureConfiguration disablePutForCreation() { + + this.creationViaPut = __ -> false; + return this; + } + + /** + * Returns a {@link ExposureConfigurer} to allow the registration of type specific + * {@link AggregateResourceHttpMethodsFilter} and {@link AssociationResourceHttpMethodsFilter}s which means the + * configured filters will only be invoked for aggregates of the given type and properties owned by that type + * respectively. + * + * @param type must not be {@literal null}. + * @return + */ + public ExposureConfigurer forDomainType(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return new TypeBasedExposureConfigurer(type); + } + + /** + * Disables the support for {@link HttpMethod#PUT} for item resources. + * + * @return + */ + public ExposureConfiguration disablePutOnItemResources() { + + this.item = item.andThen((__, httpMethods) -> httpMethods.disable(PUT)); + return this; + } + + /** + * Disables the support for {@link HttpMethod#PATCH} for item resources. + * + * @return + */ + public ExposureConfiguration disablePatchOnItemResources() { + + this.item = item.andThen((__, httpMethods) -> httpMethods.disable(PATCH)); + return this; + } + + /** + * Returns whether PUT is supported for the given {@link ResourceMetadata}. + * + * @param metadata must not be {@literal null}. + * @return + */ + public boolean allowsPutForCreation(ResourceMetadata metadata) { + + Assert.notNull(metadata, "ResourceMetadata must not be null!"); + + return creationViaPut.apply(metadata.getDomainType()); + } + + HttpMethods filter(ConfigurableHttpMethods methods, ResourceType type, ResourceMetadata metadata) { + + switch (type) { + case COLLECTION: + return collection.filter(metadata, methods); + case ITEM: + return item.filter(metadata, methods); + } + + throw new IllegalArgumentException(); + } + + HttpMethods filter(ConfigurableHttpMethods methods, PropertyAwareResourceMapping mapping) { + return property.filter(mapping, methods); + } + + private static ComposableFilter withTypeFilter(Class type, + ComposableFilter function) { + + return (metadata, httpMethods) -> // + type.isAssignableFrom(metadata.getDomainType()) // + ? function.filter(metadata, httpMethods) + : httpMethods; + } + + /** + * An intermediate {@link ExposureConfigurer} to forward the general configuration API but register the configured + * filters under the condition that the configured type is matched. + * + * @author Oliver Gierke + */ + @RequiredArgsConstructor + private class TypeBasedExposureConfigurer implements ExposureConfigurer { + + private final Class type; + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ExposureConfigurer#withCollectionExposure(org.springframework.data.rest.core.mapping.ExposureConfigurer.AggregateResourceHttpMethodsFilter) + */ + public ExposureConfigurer withCollectionExposure(AggregateResourceHttpMethodsFilter filter) { + + ExposureConfiguration config = ExposureConfiguration.this; + + config.collection = config.collection.andThen(withTypeFilter(type, filter)); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ExposureConfigurer#withItemExposure(org.springframework.data.rest.core.mapping.ExposureConfigurer.AggregateResourceHttpMethodsFilter) + */ + public ExposureConfigurer withItemExposure(AggregateResourceHttpMethodsFilter filter) { + + ExposureConfiguration config = ExposureConfiguration.this; + + config.item = config.item.andThen(withTypeFilter(type, filter)); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ExposureConfiguration.ExposureConfigurer#withAssociationExposure(java.util.function.BiFunction) + */ + @Override + public ExposureConfigurer withAssociationExposure(AssociationResourceHttpMethodsFilter filter) { + + ExposureConfiguration config = ExposureConfiguration.this; + + AssociationResourceHttpMethodsFilter typeFilter = // + (mapping, methods) -> type.isAssignableFrom(mapping.getProperty().getOwner().getType()) // + ? filter.filter(mapping, methods) // + : methods; + + config.property = config.property.andThen(typeFilter); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.ExposureConfigurer#disableCreationViaPut() + */ + @Override + public ExposureConfigurer disablePutForCreation() { + + ExposureConfiguration config = ExposureConfiguration.this; + + Function, Boolean> current = config.creationViaPut; + + config.creationViaPut = type -> this.type.isAssignableFrom(type) ? false : current.apply(type); + return this; + } + } +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfigurer.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfigurer.java new file mode 100644 index 000000000..c181e6a05 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/ExposureConfigurer.java @@ -0,0 +1,98 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import org.springframework.data.mapping.Association; +import org.springframework.http.HttpMethod; + +/** + * Configuration API to register filters to customize the supported HTTP methods by different kinds of resources. + * + * @author Oliver Gierke + * @since 2.1 + */ +public interface ExposureConfigurer { + + /** + * A filter to post-process the supported HTTP methods by aggregate resources (collection or item resource). + * + * @author Oliver Gierke + */ + interface AggregateResourceHttpMethodsFilter extends ComposableFilter { + + ConfigurableHttpMethods filter(ResourceMetadata metdata, ConfigurableHttpMethods httpMethods); + + /** + * Returns a default filter that just returns all {@link HttpMethods} as is, i.e. does not apply any filtering. + * + * @return + */ + static AggregateResourceHttpMethodsFilter none() { + return (metadata, httpMethods) -> httpMethods; + } + } + + /** + * A filter to post-process the supported HTTP methods by {@link Association} resources. + * + * @author Oliver Gierke + */ + interface AssociationResourceHttpMethodsFilter + extends ComposableFilter { + + ConfigurableHttpMethods filter(PropertyAwareResourceMapping metdata, ConfigurableHttpMethods httpMethods); + + /** + * Returns a default filter that just returns all {@link HttpMethods} as is, i.e. does not apply any filtering. + * + * @return + */ + static AssociationResourceHttpMethodsFilter none() { + return (metadata, httpMethods) -> httpMethods; + } + } + + /** + * Registers the given {@link AggregateResourceHttpMethodsFilter} to be used for collection resources. + * + * @param filter must not be {@literal null}. + * @return + */ + ExposureConfigurer withCollectionExposure(AggregateResourceHttpMethodsFilter filter); + + /** + * Registers the given {@link AggregateResourceHttpMethodsFilter} to be used for item resources. + * + * @param filter + * @return + */ + ExposureConfigurer withItemExposure(AggregateResourceHttpMethodsFilter filter); + + /** + * Registers the given {@link AssociationResourceHttpMethodsFilter}. + * + * @param filter + * @return + */ + ExposureConfigurer withAssociationExposure(AssociationResourceHttpMethodsFilter filter); + + /** + * Disables the ability to create new item resources via {@link HttpMethod#PUT}. + * + * @return + */ + ExposureConfigurer disablePutForCreation(); +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/HttpMethods.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/HttpMethods.java new file mode 100644 index 000000000..a3d523a57 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/HttpMethods.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.data.util.StreamUtils; +import org.springframework.data.util.Streamable; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; + +/** + * A collection of {@link HttpMethod}s with some convenience methods to create alternate sets of those. + * + * @author Oliver Gierke + */ +public interface HttpMethods extends Streamable { + + public static HttpMethods none() { + return ConfigurableHttpMethods.NONE; + } + + /** + * Returns a new {@link HttpMethods} with the given {@link HttpMethod}s. + * + * @param methods must not be {@literal null}. + * @return + */ + public static HttpMethods of(Collection methods) { + + Assert.notNull(methods, "HTTP methods must not be null!"); + + return ConfigurableHttpMethods.of(methods); + } + + /** + * Returns whether the given {@link HttpMethod} is contained in the current {@link HttpMethods}. + * + * @param method must not be {@literal null}. + * @return + */ + boolean contains(HttpMethod method); + + /** + * Returns an unmodifiable {@link Set} of all underlying {@link HttpMethod}s. + * + * @return + */ + default Set toSet() { + return stream().collect(StreamUtils.toUnmodifiableSet()); + } + + /** + * Returns a new {@link HttpMethods} with the given {@link HttpMethod}s added. + * + * @param method must not be {@literal null}. + * @return + */ + default HttpMethods and(HttpMethod... method) { + return of(Stream.concat(stream(), Arrays.stream(method)).collect(Collectors.toSet())); + } + + /** + * Returns a new {@link HttpMethods} instance with the given {@link HttpMethod}s removed. + * + * @param method must not be {@literal null}. + * @return + */ + default HttpMethods butWithout(HttpMethod... method) { + + List toRemove = Arrays.asList(method); + + return of(stream().filter(it -> !toRemove.contains(it)).collect(Collectors.toSet())); + } +} 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 a1b1173f4..4f3656325 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,8 +57,11 @@ class RepositoryAwareResourceMetadata implements ResourceMetadata { this.mapping = mapping; this.provider = provider; this.repositoryMetadata = repositoryMetadata; - this.crudMethodsSupportedHttpMethods = new CrudMethodsSupportedHttpMethods(repositoryMetadata.getCrudMethods(), - provider.exposeMethodsByDefault()); + + CrudMethodsSupportedHttpMethods httpMethods = new CrudMethodsSupportedHttpMethods( + repositoryMetadata.getCrudMethods(), provider.exposeMethodsByDefault()); + this.crudMethodsSupportedHttpMethods = new ConfigurationApplyingSupportedHttpMethodsAdapter( + provider.getExposureConfiguration(), this, httpMethods); } /** 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 d9b004642..df65ab114 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 @@ -155,7 +155,18 @@ public class RepositoryResourceMappings extends PersistentEntitiesResourceMappin * @since 3.1 * @see RepositoryRestConfiguration#exposeRepositoryMethodsByDefault() */ - public boolean exposeMethodsByDefault() { + boolean exposeMethodsByDefault() { return configuration.exposeRepositoryMethodsByDefault(); } + + /** + * Returns the underlying {@link ExposureConfiguration}. + * + * @return will never be {@literal null}. + * @since 3.1 + * @see RepositoryRestConfiguration#getExposureConfiguration() + */ + ExposureConfiguration getExposureConfiguration() { + return configuration.getExposureConfiguration(); + } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SupportedHttpMethods.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SupportedHttpMethods.java index 15c42bfd1..8cdd160c8 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SupportedHttpMethods.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/mapping/SupportedHttpMethods.java @@ -15,9 +15,6 @@ */ package org.springframework.data.rest.core.mapping; -import java.util.Collections; -import java.util.Set; - import org.springframework.data.mapping.PersistentProperty; import org.springframework.http.HttpMethod; @@ -34,7 +31,7 @@ public interface SupportedHttpMethods { * @param type must not be {@literal null}. * @return */ - Set getMethodsFor(ResourceType type); + HttpMethods getMethodsFor(ResourceType type); /** * Returns the supported {@link HttpMethod}s for the given {@link PersistentProperty}. @@ -42,7 +39,16 @@ public interface SupportedHttpMethods { * @param property must not be {@literal null}. * @return */ - Set getMethodsFor(PersistentProperty property); + HttpMethods getMethodsFor(PersistentProperty property); + + /** + * Returns whether {@link HttpMethod#PUT} requests are supported for item resource creation. + * + * @return + */ + default boolean allowsPutForCreation() { + return true; + } /** * Null object to abstract the absence of any support for any HTTP method. @@ -58,8 +64,8 @@ public interface SupportedHttpMethods { * @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#getSupportedHttpMethods(org.springframework.data.rest.core.mapping.ResourceType) */ @Override - public Set getMethodsFor(ResourceType resourcType) { - return Collections.emptySet(); + public HttpMethods getMethodsFor(ResourceType resourcType) { + return HttpMethods.none(); } /* @@ -67,8 +73,17 @@ public interface SupportedHttpMethods { * @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#getMethodsFor(org.springframework.data.mapping.PersistentProperty) */ @Override - public Set getMethodsFor(PersistentProperty property) { - return Collections.emptySet(); + public HttpMethods getMethodsFor(PersistentProperty property) { + return HttpMethods.none(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#allowsPutForCreation() + */ + @Override + public boolean allowsPutForCreation() { + return false; } } } 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 d39f42c89..7a2cf7dcc 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 @@ -22,7 +22,6 @@ import static org.springframework.http.HttpMethod.*; import java.util.List; import java.util.Optional; -import java.util.Set; import org.junit.Before; import org.junit.Test; @@ -151,7 +150,7 @@ public class CrudMethodsSupportedHttpMethodsUnitTests { private static void assertMethodsSupported(SupportedHttpMethods methods, ResourceType type, boolean supported, HttpMethod... httpMethods) { - Set result = methods.getMethodsFor(type); + HttpMethods result = methods.getMethodsFor(type); if (supported) { assertThat(result).containsExactlyInAnyOrder(httpMethods); diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ExposureConfigurationUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ExposureConfigurationUnitTests.java new file mode 100644 index 000000000..27a219349 --- /dev/null +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/mapping/ExposureConfigurationUnitTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.core.mapping; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.http.HttpMethod.*; + +import org.junit.Test; + +/** + * Unit tests for {@link ExposureConfiguration}. + * + * @author Oliver Gierke + */ +public class ExposureConfigurationUnitTests { + + @Test // DATAREST-948 + public void appliesTypeBasedCollectionFiltersOnlyForMatchingTypes() { + + ExposureConfiguration configuration = new ExposureConfiguration(); + + configuration.withCollectionExposure((metadata, methods) -> methods.disable(PUT)); + configuration.forDomainType(Sample.class).withCollectionExposure((metadata, methods) -> methods.disable(POST)); + + ConfigurableHttpMethods methods = ConfigurableHttpMethods.of(PUT, POST); + + assertThat(applyForCollection(configuration, methods, Sample.class)).isEmpty(); + assertThat(applyForCollection(configuration, methods, Object.class)).containsOnly(POST); + } + + @Test // DATAREST-948 + public void appliesTypeBasedItemFiltersOnlyForMatchingTypes() { + + ExposureConfiguration configuration = new ExposureConfiguration(); + + configuration.withItemExposure((metadata, methods) -> methods.disable(PUT)); + configuration.forDomainType(Sample.class).withItemExposure((metadata, methods) -> methods.disable(POST)); + + ConfigurableHttpMethods methods = ConfigurableHttpMethods.of(PUT, POST); + + assertThat(applyForItem(configuration, methods, Sample.class)).isEmpty(); + assertThat(applyForItem(configuration, methods, Object.class)).containsOnly(POST); + } + + @Test // DATAREST-948 + public void appliesTypeSpecificFilterForAggregateSubTypes() { + + ExposureConfiguration configuration = new ExposureConfiguration(); + configuration.forDomainType(Sample.class).withItemExposure((metadata, methods) -> methods.disable(POST)); + + assertThat(applyForItem(configuration, ConfigurableHttpMethods.ALL, SubSample.class)).doesNotContain(POST); + } + + private static HttpMethods applyForCollection(ExposureConfiguration config, ConfigurableHttpMethods methods, + Class type) { + return config.filter(methods, ResourceType.COLLECTION, metdataFor(type)); + } + + private static HttpMethods applyForItem(ExposureConfiguration config, ConfigurableHttpMethods methods, + Class type) { + return config.filter(methods, ResourceType.ITEM, metdataFor(type)); + } + + private static ResourceMetadata metdataFor(Class domainType) { + + ResourceMetadata metadata = mock(ResourceMetadata.class); + doReturn(domainType).when(metadata).getDomainType(); + + return metadata; + } + + static class Sample {} + + static class SubSample extends Sample {} +} diff --git a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java index 47dd137c1..2f7d68860 100755 --- a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java +++ b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/RepositoryEntityControllerIntegrationTests.java @@ -15,6 +15,7 @@ */ package org.springframework.data.rest.webmvc; +import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.not; @@ -30,11 +31,15 @@ import org.hamcrest.Matchers; import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.tests.AbstractControllerIntegrationTests; +import org.springframework.data.rest.webmvc.RepositoryEntityControllerIntegrationTests.ConfigurationCustomizer; +import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; import org.springframework.data.rest.webmvc.jpa.Address; import org.springframework.data.rest.webmvc.jpa.AddressRepository; import org.springframework.data.rest.webmvc.jpa.CreditCard; @@ -59,7 +64,7 @@ import org.springframework.web.HttpRequestMethodNotSupportedException; * @author Oliver Gierke * @author Jeremy Rickard */ -@ContextConfiguration(classes = JpaRepositoryConfig.class) +@ContextConfiguration(classes = { ConfigurationCustomizer.class, JpaRepositoryConfig.class }) @Transactional public class RepositoryEntityControllerIntegrationTests extends AbstractControllerIntegrationTests { @@ -69,6 +74,18 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll @Autowired PersistentEntityResourceAssembler assembler; @Autowired PersistentEntities entities; + @Configuration + static class ConfigurationCustomizer { + + @Bean + RepositoryRestConfigurer configurer() { + + return RepositoryRestConfigurer.withConfig(config -> { + config.getExposureConfiguration().forDomainType(Address.class).disablePutForCreation(); + }); + } + } + @Test(expected = HttpRequestMethodNotSupportedException.class) // DATAREST-217 public void returnsNotFoundForListingEntitiesIfFindAllNotExported() throws Exception { @@ -181,7 +198,7 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll RootResourceInformation request = getResourceInformation(Order.class); PersistentEntityResource persistentEntityResource = PersistentEntityResource - .build(new Order(new Person()), entities.getRequiredPersistentEntity(Order.class)).build(); + .build(new Order(new Person()), entities.getRequiredPersistentEntity(Order.class)).forCreation(); assertThat(controller.putItemResource(request, persistentEntityResource, 1L, assembler, ETag.NO_ETAG, MediaType.APPLICATION_JSON_VALUE).hasBody(), is(true)); @@ -250,5 +267,17 @@ public class RepositoryEntityControllerIntegrationTests extends AbstractControll assertThat(repository.findById(address.id)).isEmpty(); } + @Test // DATAREST-948 + public void rejectsPutForCreationIfConfigured() throws HttpRequestMethodNotSupportedException { + + RootResourceInformation request = getResourceInformation(Address.class); + PersistentEntityResource persistentEntityResource = PersistentEntityResource + .build(new Address(), entities.getRequiredPersistentEntity(Address.class)).forCreation(); + + assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class) // + .isThrownBy(() -> controller.putItemResource(request, persistentEntityResource, 1L, assembler, ETag.NO_ETAG, + MediaType.APPLICATION_JSON_VALUE)); + } + interface AddressProjection {} } diff --git a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest363Tests.java b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest363Tests.java index f0fb00e72..b113ebab7 100755 --- a/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest363Tests.java +++ b/spring-data-rest-tests/spring-data-rest-tests-jpa/src/test/java/org/springframework/data/rest/webmvc/jpa/DataRest363Tests.java @@ -37,7 +37,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index e340c9d1a..a5fc306e4 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -140,7 +140,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem HttpHeaders headers = new HttpHeaders(); SupportedHttpMethods supportedMethods = information.getSupportedMethods(); - headers.setAllow(supportedMethods.getMethodsFor(ResourceType.COLLECTION)); + headers.setAllow(supportedMethods.getMethodsFor(ResourceType.COLLECTION).toSet()); return new ResponseEntity(headers, HttpStatus.OK); } @@ -286,7 +286,7 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem HttpHeaders headers = new HttpHeaders(); SupportedHttpMethods supportedMethods = information.getSupportedMethods(); - headers.setAllow(supportedMethods.getMethodsFor(ResourceType.ITEM)); + headers.setAllow(supportedMethods.getMethodsFor(ResourceType.ITEM).toSet()); headers.put("Accept-Patch", ACCEPT_PATCH_HEADERS); return new ResponseEntity(headers, HttpStatus.OK); @@ -360,6 +360,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem resourceInformation.verifySupportedMethod(HttpMethod.PUT, ResourceType.ITEM); + if (payload.isNew()) { + resourceInformation.verifyPutForCreation(); + } + RepositoryInvoker invoker = resourceInformation.getInvoker(); Object objectToSave = payload.getContent(); eTag.verify(resourceInformation.getPersistentEntity(), objectToSave); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java index 7e43905fd..9eb2f8eb4 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RootResourceInformation.java @@ -15,13 +15,13 @@ */ package org.springframework.data.rest.webmvc; -import java.util.Collection; -import java.util.HashSet; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.repository.support.RepositoryInvoker; +import org.springframework.data.rest.core.mapping.HttpMethods; import org.springframework.data.rest.core.mapping.ResourceMetadata; import org.springframework.data.rest.core.mapping.ResourceType; import org.springframework.data.rest.core.mapping.SearchResourceMappings; @@ -101,7 +101,7 @@ public class RootResourceInformation { } SupportedHttpMethods httpMethods = resourceMetadata.getSupportedHttpMethods(); - Collection supportedMethods = httpMethods.getMethodsFor(resourceType); + HttpMethods supportedMethods = httpMethods.getMethodsFor(resourceType); if (!supportedMethods.contains(httpMethod)) { reject(httpMethod, supportedMethods); @@ -128,21 +128,28 @@ public class RootResourceInformation { } SupportedHttpMethods httpMethods = resourceMetadata.getSupportedHttpMethods(); - Collection supportedMethods = httpMethods.getMethodsFor(property); + HttpMethods supportedMethods = httpMethods.getMethodsFor(property); if (!supportedMethods.contains(httpMethod)) { reject(httpMethod, supportedMethods); } } - private static void reject(HttpMethod method, Collection supported) - throws HttpRequestMethodNotSupportedException { + public void verifyPutForCreation() throws HttpRequestMethodNotSupportedException { - Set stringMethods = new HashSet(supported.size()); + SupportedHttpMethods supportedHttpMethods = resourceMetadata.getSupportedHttpMethods(); - for (HttpMethod supportedMethod : supported) { - stringMethods.add(supportedMethod.name()); + if (!supportedHttpMethods.allowsPutForCreation()) { + reject(HttpMethod.PUT, supportedHttpMethods.getMethodsFor(ResourceType.ITEM)); } + } + + private static void reject(HttpMethod method, HttpMethods supported) throws HttpRequestMethodNotSupportedException { + + Set stringMethods = supported.butWithout(method) // + .stream() // + .map(HttpMethod::name) // + .collect(Collectors.toSet()); throw new HttpRequestMethodNotSupportedException(method.name(), stringMethods); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerUnitTests.java index ccd9d5581..a021f8c30 100755 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceControllerUnitTests.java @@ -19,12 +19,9 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Optional; -import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +36,8 @@ import org.springframework.data.repository.support.Repositories; import org.springframework.data.repository.support.RepositoryInvoker; import org.springframework.data.repository.support.RepositoryInvokerFactory; import org.springframework.data.rest.core.annotation.RestResource; +import org.springframework.data.rest.core.mapping.ConfigurableHttpMethods; +import org.springframework.data.rest.core.mapping.HttpMethods; import org.springframework.data.rest.core.mapping.PersistentEntitiesResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.core.mapping.ResourceMetadata; @@ -105,16 +104,14 @@ public class RepositoryPropertyReferenceControllerUnitTests { INSTANCE; - private static final Set ALL_METHODS = new HashSet(Arrays.asList(HttpMethod.values())); - @Override - public Set getMethodsFor(PersistentProperty property) { - return ALL_METHODS; + public HttpMethods getMethodsFor(PersistentProperty property) { + return ConfigurableHttpMethods.ALL; } @Override - public Set getMethodsFor(ResourceType type) { - return ALL_METHODS; + public HttpMethods getMethodsFor(ResourceType type) { + return ConfigurableHttpMethods.ALL; } } } diff --git a/src/main/asciidoc/customizing-sdr.adoc b/src/main/asciidoc/customizing-sdr.adoc index 42fe7ab20..5417ce838 100644 --- a/src/main/asciidoc/customizing-sdr.adoc +++ b/src/main/asciidoc/customizing-sdr.adoc @@ -63,6 +63,50 @@ public class UserEntityLookup extends EntityLookupSupport { Notice how `getResourceIdentifier(…)` returns the username to be used by the URI creation. To load entity instances by the value returned from that method, we now implement `lookupEntity(…)` by using the query method available on the `UserRepository`. +[[customizing-sdr.repository-exposure]] +== Customizing repository exposure + +By default, all public Spring Data repositories are used to expose HTTP resources as described in <>. +Package protected repository interfaces are excluded from this list, as you express its functionality is only visible to the package internally. +This can be customized by explicitly setting a `RepositoryDetectionStrategy` (usually through the enum `RepositoryDetectionStrategies`) on `RepositoryRestConfiguration`. +The following values can be configured: + +- `ALL` -- exposes all Spring Data repositories regardless of their Java visibility or annotation configuration. +- `DEFAULT` -- exposes public Spring Data repositories or ones explicitly annotated with `@RepositoryRestResource` and its `exported` attribute not set to `false`. +- `VISIBILITY` -- exposes only public Spring Data repositories regardless of annotation configuration. +- `ANNOTATED` -- only exposes Spring Data repositories explicitly annotated with `@RepositoryRestResource` and its `exported` attribute not set to `false`. + +If you need custom rules to apply, simply implement `RepositoryDetectionStrategy` manually. + +[[customizing-sdr.http-methods]] +== Customizing supported HTTP methods + +[[customizing-sdr.http-methods.default-exposure]] +=== Customizing default exposure + +By default, Spring Data REST exposes HTTP resources and methods as described in <> based on which CRUD methods the repository exposes. +The repositories don't need to extend `CrudRepository` but can also selectively declare methods described in aforementioned section and the resource exposure will follow. +E.g. if a repository does not expose a `delete(…)` method, an HTTP `DELETE` will not be supported for item resources. + +If you need to declare a method for internal use but don't want it to trigger the HTTP method exposure, the repository method can be annotated with `@RestResource(exported = false)`. +Which methods to annotate like that to remove support for which HTTP method is described in <>. + +Sometimes managing the exposure on the method level is not fine-grained enough. +E.g. the `save(…)` method is used to back `POST` on collection resources, as well as `PUT` and `PATCH` on item resources. +To selectively define which HTTP methods are supposed to be exposed, you can use `RepositoryRestConfiguration.getExposureConfiguration()`. + +The class exposes a Lambda based API to define both global and type-based rules: + +[source, java] +---- +ExposureConfiguration config = repositoryRestConfiguration.getExposureConfiguration(); + +config.forDomainType(User.class).disablePutForCreation(); <1> +config.withItemExposure((metadata, httpMethods) -> httpMethods.disable(HttpMethod.PATCH)); <2> +---- +<1> Disables the support for HTTP `PUT` to create item resources directly. +<2> Disables the support for HTTP `PATCH` on all item resources. + include::configuring-the-rest-url-path.adoc[leveloffset=+1] include::adding-sdr-to-spring-mvc-app.adoc[leveloffset=+1] diff --git a/src/main/asciidoc/repository-resources.adoc b/src/main/asciidoc/repository-resources.adoc index c09ece4ea..102c787bc 100644 --- a/src/main/asciidoc/repository-resources.adoc +++ b/src/main/asciidoc/repository-resources.adoc @@ -17,6 +17,17 @@ For this repository, Spring Data REST exposes a collection resource at `/orders` By default the HTTP methods to interact with these resources map to the according methods of `CrudRepository`. Read more on that in the sections on <> and <>. +[[repository-resources.methods]] +=== Repository methods exposure + +Which HTTP resources are exposed for a certain repository is mostly driven by the structure of the repository. +In other words, the resource exposure will follow which methods you have exposed on the repository. +If you extend `CrudRepository` you usually expose all methods required to expose all HTTP resources we can register by default. +Each of the resources listed below will define which of the methods need to be present so that a particular HTTP method can be exposed for each of the resources. +That means, that repositories that are not exposing those methods -- either by not declaring them at all or explicitly using `@RestResource(exported = false)` -- won't expose those HTTP methods on those resources. + +For details on how to tweak the default method exposure or dedicated HTTP methods individually see + [[repository-resources.default-status-codes]] === Default Status Codes @@ -72,7 +83,18 @@ Collections resources support both `GET` and `POST`. All other HTTP methods caus ==== `GET` -The `GET` method returns all entities the repository serves through its `findAll(…)` method. If the repository is a paging repository, we include the pagination links (if the total number of results exceeds the page size) and additional page metadata. +Returns all entities the repository servers through its `findAll(…)` method. +If the repository is a paging repository we include the pagination links if necessary and additional page metadata. + +===== Methods used for invocation + +The following methods are used if present (decending order): + +- `findAll(Pageable)` +- `findAll(Sort)` +- `findAll()` + +For more information on the default exposure of methods, see <>. ===== Parameters @@ -105,10 +127,28 @@ The `GET` method supports a single link for discovering related resources: The `HEAD` method returns whether the collection resource is available. It has no status codes, media types, or related resources. +===== Methods used for invocation + +The following methods are used if present (decending order): + +- `findAll(Pageable)` +- `findAll(Sort)` +- `findAll()` + +For more information on the default exposure of methods, see <>. + ==== `POST` The `POST` method creates a new entity from the given request body. +===== Methods used for invocation + +The following methods are used if present (decending order): + +- `save(…)` + +For more information on the default exposure of methods, see <>. + ===== Custom Status Codes The `POST` method has only one custom status code: @@ -135,6 +175,14 @@ Item resources generally support `GET`, `PUT`, `PATCH`, and `DELETE`, unless exp The `GET` method returns a single entity. +===== Methods used for invocation + +The following methods are used if present (decending order): + +- `findById(…)` + +For more information on the default exposure of methods, see <>. + ===== Custom Status Codes The `GET` method has only one custom status code: @@ -156,10 +204,26 @@ For every association of the domain type, we expose links named after the associ The `HEAD` method returns whether the item resource is available. It has no status codes, media types, or related resources. +===== Methods used for invocation + +The following methods are used if present (decending order): + +- `findById(…)` + +For more information on the default exposure of methods, see <>. + ==== `PUT` The `PUT` method replaces the state of the target resource with the supplied request body. +===== Methods used for invocation + +The following methods are used if present (decending order): + +- `save(…)` + +For more information on the default exposure of methods, see <>. + ===== Custom Status Codes The `PUT` method has only one custom status code: @@ -177,6 +241,14 @@ The `PUT` method supports the following media types: The `PATCH` method is similar to the `PUT` method but partially updates the resources state. +===== Methods used for invocation + +The following methods are used if present (decending order): + +- `save(…)` + +For more information on the default exposure of methods, see <>. + ===== Custom Status Codes The `PATCH` method has only one custom status code: @@ -196,6 +268,16 @@ The `PATCH` method supports the following media types: The `DELETE` method deletes the resource exposed. +===== Methods used for invocation + +The following methods are used if present (decending order): + +- `delete(T)` +- `delete(ID)` +- `delete(Iterable)` + +For more information on the default exposure of methods, see <>. + ===== Custom Status Codes The `DELETE` method has only one custom status code: diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 40b741837..99e1a842d 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -5,7 +5,6 @@ Changes in version 3.1.0.M3 (2018-05-17) ---------------------------------------- * DATAREST-1231 - Upgrade to Solr 7.2.1. * DATAREST-1230 - Release 3.1 M3 (Lovelace). -* DATAREST-948 - Switch to disable PUT for creation on item resources by default. Changes in version 3.0.7.RELEASE (2018-05-08)