DATAREST-948 - Introduced ExposureConfiguration to allow customizing the exposure of HTTP methods of repositories.
ExposureConfiguration exposes methods to register AggregateResourceHttpMethodsFilter and AssociationResourceHttpMethodsFilter (both applied by type or globally) to customize the supported HTTP methods by collection, item and association resources. It also provides shortcuts for common use cases like disabling PUT for item resources etc.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<T, S> {
|
||||
|
||||
S filter(T first, S second);
|
||||
|
||||
default ComposableFilter<T, S> andThen(ComposableFilter<T, S> filter) {
|
||||
return (first, second) -> filter.filter(first, filter(first, second));
|
||||
}
|
||||
}
|
||||
@@ -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<HttpMethod> 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<HttpMethod> 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<HttpMethod> 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<HttpMethod> iterator() {
|
||||
return methods.iterator();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<HttpMethod> 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<HttpMethod> getMethodsFor(PersistentProperty<?> property) {
|
||||
public HttpMethods getMethodsFor(PersistentProperty<?> property) {
|
||||
|
||||
if (!property.isAssociation()) {
|
||||
return Collections.emptySet();
|
||||
return HttpMethods.none();
|
||||
}
|
||||
|
||||
Set<HttpMethod> methods = new HashSet<HttpMethod>();
|
||||
@@ -133,7 +132,7 @@ public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods {
|
||||
methods.add(POST);
|
||||
}
|
||||
|
||||
return methods;
|
||||
return HttpMethods.of(methods);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<ResourceMetadata, ConfigurableHttpMethods> collection = AggregateResourceHttpMethodsFilter
|
||||
.none();
|
||||
private ComposableFilter<ResourceMetadata, ConfigurableHttpMethods> item = AggregateResourceHttpMethodsFilter.none();
|
||||
private ComposableFilter<PropertyAwareResourceMapping, ConfigurableHttpMethods> property = AssociationResourceHttpMethodsFilter
|
||||
.none();
|
||||
|
||||
private Function<Class<?>, 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 <T> ComposableFilter<ResourceMetadata, ConfigurableHttpMethods> withTypeFilter(Class<?> type,
|
||||
ComposableFilter<ResourceMetadata, ConfigurableHttpMethods> 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<Class<?>, Boolean> current = config.creationViaPut;
|
||||
|
||||
config.creationViaPut = type -> this.type.isAssignableFrom(type) ? false : current.apply(type);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ResourceMetadata, ConfigurableHttpMethods> {
|
||||
|
||||
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<PropertyAwareResourceMapping, ConfigurableHttpMethods> {
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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<HttpMethod> {
|
||||
|
||||
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<HttpMethod> 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<HttpMethod> 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<HttpMethod> toRemove = Arrays.asList(method);
|
||||
|
||||
return of(stream().filter(it -> !toRemove.contains(it)).collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HttpMethod> 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<HttpMethod> 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<HttpMethod> 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<HttpMethod> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HttpMethod> result = methods.getMethodsFor(type);
|
||||
HttpMethods result = methods.getMethodsFor(type);
|
||||
|
||||
if (supported) {
|
||||
assertThat(result).containsExactlyInAnyOrder(httpMethods);
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Object>(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<Object>(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);
|
||||
|
||||
@@ -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<HttpMethod> 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<HttpMethod> supportedMethods = httpMethods.getMethodsFor(property);
|
||||
HttpMethods supportedMethods = httpMethods.getMethodsFor(property);
|
||||
|
||||
if (!supportedMethods.contains(httpMethod)) {
|
||||
reject(httpMethod, supportedMethods);
|
||||
}
|
||||
}
|
||||
|
||||
private static void reject(HttpMethod method, Collection<HttpMethod> supported)
|
||||
throws HttpRequestMethodNotSupportedException {
|
||||
public void verifyPutForCreation() throws HttpRequestMethodNotSupportedException {
|
||||
|
||||
Set<String> stringMethods = new HashSet<String>(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<String> stringMethods = supported.butWithout(method) //
|
||||
.stream() //
|
||||
.map(HttpMethod::name) //
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
throw new HttpRequestMethodNotSupportedException(method.name(), stringMethods);
|
||||
}
|
||||
|
||||
@@ -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<HttpMethod> ALL_METHODS = new HashSet<HttpMethod>(Arrays.asList(HttpMethod.values()));
|
||||
|
||||
@Override
|
||||
public Set<HttpMethod> getMethodsFor(PersistentProperty<?> property) {
|
||||
return ALL_METHODS;
|
||||
public HttpMethods getMethodsFor(PersistentProperty<?> property) {
|
||||
return ConfigurableHttpMethods.ALL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<HttpMethod> getMethodsFor(ResourceType type) {
|
||||
return ALL_METHODS;
|
||||
public HttpMethods getMethodsFor(ResourceType type) {
|
||||
return ConfigurableHttpMethods.ALL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,50 @@ public class UserEntityLookup extends EntityLookupSupport<User> {
|
||||
|
||||
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 <<repository-resources>>.
|
||||
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 <<repository-resources>> 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 <<repository-resources>>.
|
||||
|
||||
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]
|
||||
|
||||
@@ -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 <<repository-resources.collection-resource,collection resources>> and <<repository-resources.item-resource,item resources>>.
|
||||
|
||||
[[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 <<repository-resources.methods>>.
|
||||
|
||||
===== 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 <<repository-resources.methods>>.
|
||||
|
||||
==== `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 <<repository-resources.methods>>.
|
||||
|
||||
===== 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 <<repository-resources.methods>>.
|
||||
|
||||
===== 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 <<repository-resources.methods>>.
|
||||
|
||||
==== `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 <<repository-resources.methods>>.
|
||||
|
||||
===== 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 <<repository-resources.methods>>.
|
||||
|
||||
===== 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 <<repository-resources.methods>>.
|
||||
|
||||
===== Custom Status Codes
|
||||
|
||||
The `DELETE` method has only one custom status code:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user