DATAREST-409 - Port RepositoryInvoker API to Spring Data Commons.

Introduced SupportedHttpMethods abstraction to be able to test the exposure of HTTp methods based on a CrudMethods instance only. Moved ResourceType to the core module.
This commit is contained in:
Oliver Gierke
2014-11-12 19:57:04 +01:00
parent dbeec0a5de
commit 458cdfa7e8
28 changed files with 486 additions and 1330 deletions

View File

@@ -1,71 +0,0 @@
package org.springframework.data.rest.core.invoke;
import java.lang.reflect.Method;
/**
* Represents one of the CRUD methods supported by
* {@link org.springframework.data.repository.PagingAndSortingRepository} or
* {@link org.springframework.data.repository.CrudRepository}.
*
* @author Jon Brisbin
*/
public enum CrudMethod {
COUNT, DELETE_ALL, DELETE_ONE, DELETE_SOME, FIND_ALL, FIND_ONE, FIND_SOME, SAVE_ONE, SAVE_SOME;
/**
* Get an enum from a {@link Method}. Narrow down overridden methods by looking for {@link Iterable} in the first
* parameter, which tells us it is a '_SOME' type.
*
* @param m The CRUD method from the repository interface.
* @return An enum representing which CRUD operation this method represents.
*/
public static CrudMethod fromMethod(Method m) {
String s = m.getName();
Class<?>[] paramTypes = m.getParameterTypes();
boolean some = (paramTypes.length > 0 && Iterable.class.isAssignableFrom(paramTypes[0]));
if ("count".equals(s)) {
return COUNT;
} else if ("delete".equals(s)) {
return (some ? DELETE_SOME : DELETE_ONE);
} else if ("deleteAll".equals(s)) {
return DELETE_ALL;
} else if ("findAll".equals(s)) {
return (some ? FIND_SOME : FIND_ALL);
} else if ("findOne".equals(s)) {
return FIND_ONE;
} else if ("save".equals(s)) {
return (some ? SAVE_SOME : SAVE_ONE);
} else {
return null;
}
}
/**
* Turn this enum into a method name.
*
* @return The method name as a string.
*/
public String toMethodName() {
switch (this) {
case COUNT:
return "count";
case DELETE_ALL:
return "deleteAll";
case DELETE_ONE:
case DELETE_SOME:
return "delete";
case FIND_ALL:
case FIND_SOME:
return "findAll";
case FIND_ONE:
return "findOne";
case SAVE_ONE:
case SAVE_SOME:
return "save";
default:
return null;
}
}
}

View File

@@ -1,126 +0,0 @@
/*
* Copyright 2013-2014 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.invoke;
import java.io.Serializable;
import java.lang.reflect.Method;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryInformation;
/**
* {@link RepositoryInvoker} to shortcut execution of CRUD methods into direct calls on a {@link CrudRepository}. Used
* to avoid reflection overhead introduced by the base class if we know we work with a {@link CrudRepository}.
*
* @author Oliver Gierke
*/
class CrudRepositoryInvoker extends ReflectionRepositoryInvoker {
private final CrudRepository<Object, Serializable> repository;
private final CrudMethods crudMethods;
private final boolean customSaveMethod;
private final boolean customFindOneMethod;
private final boolean customDeleteMethod;
/**
* Creates a new {@link CrudRepositoryInvoker} for the given {@link CrudRepository}, {@link RepositoryInformation} and
* {@link ConversionService}.
*
* @param repository must not be {@literal null}.
* @param information must not be {@literal null}.
* @param conversionService must not be {@literal null}.
*/
public CrudRepositoryInvoker(CrudRepository<Object, Serializable> repository, RepositoryInformation information,
ConversionService conversionService) {
super(repository, information, conversionService);
this.repository = repository;
this.crudMethods = information.getCrudMethods();
this.customSaveMethod = isRedeclaredMethod(crudMethods.getSaveMethod());
this.customFindOneMethod = isRedeclaredMethod(crudMethods.getFindOneMethod());
this.customDeleteMethod = isRedeclaredMethod(crudMethods.getDeleteMethod());
}
/**
* Invokes the method equivalent to {@link CrudRepository#findAll()}.
*
* @return
*/
protected Iterable<Object> invokeFindAll() {
return repository.findAll();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Sort)
*/
@Override
public Iterable<Object> invokeFindAll(Sort pageable) {
return repository.findAll();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Pageable)
*/
@Override
public Iterable<Object> invokeFindAll(Pageable pageable) {
return repository.findAll();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeFindOne(java.io.Serializable)
*/
@Override
@SuppressWarnings("unchecked")
public <T> T invokeFindOne(Serializable id) {
return customFindOneMethod ? super.<T> invokeFindOne(id) : (T) repository.findOne(convertId(id));
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.ReflectionRepositoryInvoker#invokeSave(java.lang.Object)
*/
@Override
public <T> T invokeSave(T entity) {
return customSaveMethod ? super.invokeSave(entity) : repository.save(entity);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeDelete(java.io.Serializable)
*/
@Override
public void invokeDelete(Serializable id) {
if (customDeleteMethod) {
super.invokeDelete(id);
} else {
repository.delete(convertId(id));
}
}
private boolean isRedeclaredMethod(Method method) {
return !method.getDeclaringClass().equals(CrudRepository.class);
}
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright 2013 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.invoke;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.util.Assert;
/**
* Default implementation of {@link RepositoryInvokerFactory} to inspect the requested repository type and create a
* matching {@link RepositoryInvoker} that suits the repository best. That means, the more concrete the base interface
* of the repository is, the more concrete will the actual invoker become - which means it will favor concrete method
* invocations over reflection ones.
*
* @author Oliver Gierke
*/
public class DefaultRepositoryInvokerFactory implements RepositoryInvokerFactory {
private final Repositories repositories;
private final ConversionService conversionService;
private final Map<Class<?>, RepositoryInvoker> invokers;
/**
* Creates a new {@link DefaultRepositoryInvokerFactory} for the given {@link Repositories} and
* {@link ConversionService}.
*
* @param repositories must not be {@literal null}.
* @param conversionService must not be {@literal null}.
*/
public DefaultRepositoryInvokerFactory(Repositories repositories, ConversionService conversionService) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(conversionService, "ConversionService must not be null!");
this.repositories = repositories;
this.conversionService = conversionService;
this.invokers = new HashMap<Class<?>, RepositoryInvoker>();
}
/**
* Creates a {@link RepositoryInvoker} for the repository managing the given domain type.
*
* @param domainType
* @return
*/
@SuppressWarnings("unchecked")
private RepositoryInvoker prepareInvokers(Class<?> domainType) {
Object repository = repositories.getRepositoryFor(domainType);
RepositoryInformation information = repositories.getRepositoryInformationFor(domainType);
if (repository instanceof PagingAndSortingRepository) {
return new PagingAndSortingRepositoryInvoker((PagingAndSortingRepository<Object, Serializable>) repository,
information, conversionService);
} else if (repository instanceof CrudRepository) {
return new CrudRepositoryInvoker((CrudRepository<Object, Serializable>) repository, information,
conversionService);
} else {
return new ReflectionRepositoryInvoker(repository, information, conversionService);
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvokerFactory#getInvokerFor(java.lang.Class)
*/
@Override
public RepositoryInvoker getInvokerFor(Class<?> domainType) {
RepositoryInvoker invoker = invokers.get(domainType);
if (invoker != null) {
return invoker;
}
invoker = prepareInvokers(domainType);
invokers.put(domainType, invoker);
return invoker;
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2013 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.invoke;
import java.io.Serializable;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.core.RepositoryInformation;
/**
* A special {@link RepositoryInvoker} that shortcuts invocations to methods on {@link PagingAndSortingRepository} to
* avoid reflection overhead introduced by the superclass.
*
* @author Oliver Gierke
*/
class PagingAndSortingRepositoryInvoker extends CrudRepositoryInvoker {
private final PagingAndSortingRepository<Object, Serializable> repository;
/**
* Creates a new {@link PagingAndSortingRepositoryInvoker} using the given repository, {@link RepositoryInformation}
* and {@link ConversionService}.
*
* @param repository must not be {@literal null}.
* @param information must not be {@literal null}.
* @param conversionService must not be {@literal null}.
*/
public PagingAndSortingRepositoryInvoker(PagingAndSortingRepository<Object, Serializable> repository,
RepositoryInformation information, ConversionService conversionService) {
super(repository, information, conversionService);
this.repository = repository;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.CrudRepositoryInvoker#invokeFindAll(org.springframework.data.domain.Sort)
*/
@Override
public Iterable<Object> invokeFindAll(Sort sort) {
return sort == null ? invokeFindAll() : repository.findAll(sort);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.CrudRepositoryInvoker#invokeFindAll(org.springframework.data.domain.Pageable)
*/
@Override
public Iterable<Object> invokeFindAll(Pageable pageable) {
return pageable == null ? invokeFindAll() : repository.findAll(pageable);
}
}

View File

@@ -1,296 +0,0 @@
/*
* Copyright 2013-2014 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.invoke;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.hateoas.core.AnnotationAttribute;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* Base {@link RepositoryInvoker} using reflection to invoke methods on Spring Data Repositories.
*
* @author Oliver Gierke
*/
class ReflectionRepositoryInvoker implements RepositoryInvoker {
private static final AnnotationAttribute PARAM_ANNOTATION = new AnnotationAttribute(Param.class);
private final Object repository;
private final CrudMethods methods;
private final RepositoryInformation information;
private final ConversionService conversionService;
/**
* Creates a new {@link ReflectionRepositoryInvoker} for the given repository, {@link RepositoryInformation} and
* {@link ConversionService}.
*
* @param repository must not be {@literal null}.
* @param information must not be {@literal null}.
* @param conversionService must not be {@literal null}.
*/
public ReflectionRepositoryInvoker(Object repository, RepositoryInformation information,
ConversionService conversionService) {
Assert.notNull(repository, "Repository must not be null!");
Assert.notNull(information, "RepositoryInformation must not be null!");
Assert.notNull(conversionService, "ConversionService must not be null!");
this.repository = repository;
this.methods = information.getCrudMethods();
this.information = information;
this.conversionService = conversionService;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvocationInformation#hasFindAllMethod()
*/
@Override
public boolean hasFindAllMethod() {
return methods.hasFindAllMethod();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvocationInformation#exposesFindAll()
*/
@Override
public boolean exposesFindAll() {
return methods.hasFindAllMethod() && exposes(methods.getFindAllMethod());
}
/* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Sort)
*/
@Override
@SuppressWarnings("unchecked")
public Iterable<Object> invokeFindAll(Sort sort) {
return (Iterable<Object>) invoke(methods.getFindAllMethod(), sort);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Pageable)
*/
@Override
public Iterable<Object> invokeFindAll(Pageable pageable) {
if (!exposesFindAll()) {
return Collections.emptyList();
}
Method method = methods.getFindAllMethod();
Class<?>[] types = method.getParameterTypes();
if (types.length == 0) {
return invoke(method);
}
if (Sort.class.isAssignableFrom(types[0])) {
return invoke(method, pageable == null ? null : pageable.getSort());
}
return invoke(method, pageable);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvocationInformation#hasSaveMethod()
*/
@Override
public boolean hasSaveMethod() {
return methods.hasSaveMethod();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvocationInformation#exposesSave()
*/
@Override
public boolean exposesSave() {
return methods.hasSaveMethod() && exposes(methods.getSaveMethod());
}
/* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeSave(java.lang.Object)
*/
@Override
public <T> T invokeSave(T object) {
return invoke(methods.getSaveMethod(), object);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvocationInformation#hasFindOneMethod()
*/
@Override
public boolean hasFindOneMethod() {
return methods.hasFindOneMethod();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvocationInformation#exposesFindOne()
*/
@Override
public boolean exposesFindOne() {
return methods.hasFindOneMethod() && exposes(methods.getFindOneMethod());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeFindOne(java.io.Serializable)
*/
@Override
public <T> T invokeFindOne(Serializable id) {
return invoke(methods.getFindOneMethod(), convertId(id));
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvocationInformation#hasDeleteMethod()
*/
@Override
public boolean hasDeleteMethod() {
return methods.hasDelete();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvocationInformation#exposesDelete()
*/
@Override
public boolean exposesDelete() {
return methods.hasDelete() && exposes(methods.getDeleteMethod());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeDelete(java.io.Serializable)
*/
@Override
@SuppressWarnings("unchecked")
public void invokeDelete(Serializable id) {
Method method = methods.getDeleteMethod();
Class<?> parameterType = method.getParameterTypes()[0];
List<Class<? extends Serializable>> idTypes = Arrays.asList(information.getIdType(), Serializable.class);
if (idTypes.contains(parameterType)) {
invoke(method, convertId(id));
} else {
invoke(method, invokeFindOne(id));
}
}
private boolean exposes(Method method) {
RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class);
return annotation == null ? true : annotation.exported();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.invoke.RepositoryInvoker#invokeQueryMethod(java.lang.reflect.Method, java.util.Map, org.springframework.data.domain.Pageable, org.springframework.data.domain.Sort)
*/
@Override
public Object invokeQueryMethod(Method method, Map<String, String[]> parameters, Pageable pageable, Sort sort) {
return invoke(method, prepareParameters(method, parameters, pageable, sort));
}
private Object[] prepareParameters(Method method, Map<String, String[]> rawParameters, Pageable pageable, Sort sort) {
List<MethodParameter> parameters = new MethodParameters(method, PARAM_ANNOTATION).getParameters();
if (parameters.isEmpty()) {
return new Object[0];
}
Object[] result = new Object[parameters.size()];
Sort sortToUse = pageable == null ? sort : pageable.getSort();
for (int i = 0; i < result.length; i++) {
MethodParameter param = parameters.get(i);
Class<?> targetType = param.getParameterType();
if (Pageable.class.isAssignableFrom(targetType)) {
result[i] = pageable;
} else if (Sort.class.isAssignableFrom(targetType)) {
result[i] = sortToUse;
} else {
String parameterName = param.getParameterName();
if (!StringUtils.hasText(parameterName)) {
throw new IllegalArgumentException("No @Param annotation found on query method " + method.getName()
+ " for parameter " + parameterName);
}
String[] parameterValue = rawParameters.get(parameterName);
Object value = parameterValue == null ? null : parameterValue.length == 1 ? parameterValue[0] : parameterValue;
result[i] = conversionService.convert(value, TypeDescriptor.forObject(value), new TypeDescriptor(param));
}
}
return result;
}
/**
* Invokes the given method with the given arguments on the backing repository.
*
* @param method
* @param arguments
* @return
*/
@SuppressWarnings("unchecked")
private <T> T invoke(Method method, Object... arguments) {
ReflectionUtils.makeAccessible(method);
return (T) ReflectionUtils.invokeMethod(method, repository, arguments);
}
/**
* Converts the given id into the id type of the backing repository.
*
* @param id must not be {@literal null}.
* @return
*/
protected Serializable convertId(Serializable id) {
Assert.notNull(id, "Id must not be null!");
return conversionService.convert(id, information.getIdType());
}
}

View File

@@ -1,80 +0,0 @@
/*
* Copyright 2013-2014 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.invoke;
/**
* Meta-information about the methods a repository exposes.
*
* @author Oliver Gierke
*/
public interface RepositoryInvocationInformation {
/**
* Returns whether the repository has a method to save objects.
*
* @return
*/
boolean hasSaveMethod();
/**
* Returns whether the repository exposes the save method.
*
* @return
*/
boolean exposesSave();
/**
* Returns whether the repository has a method to delete objects.
*
* @return
*/
boolean hasDeleteMethod();
/**
* Returns whether the repository exposes the delete method.
*
* @return
*/
boolean exposesDelete();
/**
* Returns whether the repository has a method to find a single object.
*
* @return
*/
boolean hasFindOneMethod();
/**
* Returns whether the repository exposes the method to find a single object.
*
* @return
*/
boolean exposesFindOne();
/**
* Returns whether the repository has a method to find all objects.
*
* @return
*/
boolean hasFindAllMethod();
/**
* Returns whether the repository exposes the method to find all objects.
*
* @return
*/
boolean exposesFindAll();
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright 2013 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.invoke;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
/**
* @author Oliver Gierke
*/
public interface RepositoryInvoker extends RepositoryInvocationInformation {
<T> T invokeSave(T object);
<T> T invokeFindOne(Serializable id);
Iterable<Object> invokeFindAll(Pageable pageable);
Iterable<Object> invokeFindAll(Sort sort);
void invokeDelete(Serializable serializable);
Object invokeQueryMethod(Method method, Map<String, String[]> parameters, Pageable pageable, Sort sort);
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2013 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.invoke;
/**
* Interface for a factory to create {@link RepositoryInvoker} instances for repositories managing a particular domain
* type.
*
* @author Oliver Gierke
*/
public interface RepositoryInvokerFactory {
/**
* Returns the {@link RepositoryInvoker} for a repository managing the given domain type.
*
* @param domainType must not be {@literal null}.
* @return
*/
RepositoryInvoker getInvokerFor(Class<?> domainType);
}

View File

@@ -0,0 +1,209 @@
/*
* Copyright 2014 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.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
/**
* {@link SupportedHttpMethods} that are determined by a {@link CrudMethods} instance.
*
* @author Oliver Gierke
* @since 2.3
*/
public class CrudMethodsSupportedHttpMethods implements SupportedHttpMethods {
private final ExposureAwareCrudMethods exposedMethods;
/**
* Creates a new {@link CrudMethodsSupportedHttpMethods} for the given {@link CrudMethods}.
*
* @param crudMethods must not be {@literal null}.
*/
public CrudMethodsSupportedHttpMethods(CrudMethods crudMethods) {
Assert.notNull(crudMethods, "CrudMethods must not be null!");
this.exposedMethods = new DefaultExposureAwareCrudMethods(crudMethods);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#supports(org.springframework.http.HttpMethod, org.springframework.data.rest.core.mapping.ResourceType)
*/
@Override
public boolean supports(HttpMethod method, ResourceType type) {
Assert.notNull(method, "HTTP method must not be null!");
Assert.notNull(type, "Resource type must not be null!");
return getMethodsFor(type).contains(method);
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#getSupportedHttpMethods(org.springframework.data.rest.core.mapping.ResourceType)
*/
@Override
public Set<HttpMethod> getMethodsFor(ResourceType resourcType) {
Assert.notNull(resourcType, "Resource type must not be null!");
Set<HttpMethod> methods = new HashSet<HttpMethod>();
methods.add(HttpMethod.OPTIONS);
switch (resourcType) {
case COLLECTION:
if (exposedMethods.exposesFindAll()) {
methods.add(HttpMethod.GET);
methods.add(HttpMethod.HEAD);
}
if (exposedMethods.exposesSave()) {
methods.add(HttpMethod.POST);
}
break;
case ITEM:
if (exposedMethods.exposesDelete() && exposedMethods.exposesFindOne()) {
methods.add(HttpMethod.DELETE);
}
if (exposedMethods.exposesFindOne()) {
methods.add(HttpMethod.GET);
methods.add(HttpMethod.HEAD);
}
if (exposedMethods.exposesSave()) {
methods.add(HttpMethod.PUT);
methods.add(HttpMethod.PATCH);
}
break;
default:
throw new IllegalArgumentException(String.format("Unsupported resource type %s!", resourcType));
}
return Collections.unmodifiableSet(methods);
}
/**
* @author Oliver Gierke
*/
private static class DefaultExposureAwareCrudMethods implements ExposureAwareCrudMethods {
private final CrudMethods crudMethods;
/**
* @param exposedMethods
*/
public DefaultExposureAwareCrudMethods(CrudMethods crudMethods) {
this.crudMethods = crudMethods;
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ExposureAwareCrudMethods#exposesSave()
*/
@Override
public boolean exposesSave() {
return exposes(crudMethods.getSaveMethod());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ExposureAwareCrudMethods#exposesDelete()
*/
@Override
public boolean exposesDelete() {
return exposes(crudMethods.getDeleteMethod());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ExposureAwareCrudMethods#exposesFindOne()
*/
@Override
public boolean exposesFindOne() {
return exposes(crudMethods.getFindOneMethod());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ExposureAwareCrudMethods#exposesFindAll()
*/
@Override
public boolean exposesFindAll() {
return exposes(crudMethods.getFindAllMethod());
}
private static boolean exposes(Method method) {
if (method == null) {
return false;
}
RestResource annotation = AnnotationUtils.findAnnotation(method, RestResource.class);
return annotation == null ? true : annotation.exported();
}
}
/**
* @author Oliver Gierke
*/
interface ExposureAwareCrudMethods {
/**
* Returns whether the repository exposes the save method.
*
* @return
*/
boolean exposesSave();
/**
* Returns whether the repository exposes the delete method.
*
* @return
*/
boolean exposesDelete();
/**
* Returns whether the repository exposes the method to find a single object.
*
* @return
*/
boolean exposesFindOne();
/**
* Returns whether the repository exposes the method to find all objects.
*
* @return
*/
boolean exposesFindAll();
}
}

View File

@@ -21,6 +21,7 @@ import java.util.Map;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.rest.core.mapping.SupportedHttpMethods.NoSupportedMethods;
/**
* {@link ResourceMetadata} based on a {@link PersistentEntity}.
@@ -100,4 +101,13 @@ public class MappingResourceMetadata extends TypeBasedCollectionResourceMapping
public SearchResourceMappings getSearchResourceMappings() {
return new SearchResourceMappings(Collections.<MethodResourceMapping> emptyList());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ResourceMetadata#getSupportedHttpMethods()
*/
@Override
public SupportedHttpMethods getSupportedHttpMethods() {
return NoSupportedMethods.INSTANCE;
}
}

View File

@@ -34,6 +34,7 @@ class RepositoryAwareResourceInformation implements ResourceMetadata {
private final CollectionResourceMapping mapping;
private final RepositoryResourceMappings provider;
private final RepositoryMetadata repositoryInterface;
private final SupportedHttpMethods crudMethodsSupportedHttpMethods;
/**
* Creates a new {@link RepositoryAwareResourceInformation} for the given {@link Repositories},
@@ -56,6 +57,7 @@ class RepositoryAwareResourceInformation implements ResourceMetadata {
this.mapping = mapping;
this.provider = provider;
this.repositoryInterface = repositoryMetadata;
this.crudMethodsSupportedHttpMethods = new CrudMethodsSupportedHttpMethods(repositoryMetadata.getCrudMethods());
}
/**
@@ -186,4 +188,13 @@ class RepositoryAwareResourceInformation implements ResourceMetadata {
public SearchResourceMappings getSearchResourceMappings() {
return provider.getSearchResourceMappings(repositoryInterface.getRepositoryInterface());
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.ResourceMetadata#getSupportedHttpMethods()
*/
@Override
public SupportedHttpMethods getSupportedHttpMethods() {
return crudMethodsSupportedHttpMethods;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013 the original author or authors.
* Copyright 2013-2014 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.
@@ -16,9 +16,10 @@
package org.springframework.data.rest.core.mapping;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.http.HttpMethod;
/**
* Interface for metadata of resources exposed throught the system.
* Interface for metadata of resources exposed through the system.
*
* @author Oliver Gierke
*/
@@ -62,4 +63,12 @@ public interface ResourceMetadata extends CollectionResourceMapping {
* @return
*/
SearchResourceMappings getSearchResourceMappings();
/**
* Returns the supported {@link HttpMethod}s for the given {@link ResourceType}.
*
* @param resourcType must not be {@literal null}.
* @return
*/
SupportedHttpMethods getSupportedHttpMethods();
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.rest.webmvc;
package org.springframework.data.rest.core.mapping;
/**
* An enum listing all supported resource types.

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2014 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.Collections;
import java.util.Set;
import org.springframework.http.HttpMethod;
/**
* An API to discover the {@link HttpMethod}s supported on a given {@link ResourceType}.
*
* @author Oliver Gierke
*/
public interface SupportedHttpMethods {
/**
* Returns whether the given {@link HttpMethod} is supported for the given {@link ResourceType}.
*
* @param httpMethod must not be {@literal null}.
* @param resourceType must not be {@literal null}.
* @return
*/
boolean supports(HttpMethod method, ResourceType type);
/**
* Returns the supported {@link HttpMethod}s for the given {@link ResourceType}.
*
* @param resourcType must not be {@literal null}.
* @return
*/
Set<HttpMethod> getMethodsFor(ResourceType resourcType);
/**
* Null object to abstract the absence of any support for any HTTP method.
*
* @author Oliver Gierke
*/
enum NoSupportedMethods implements SupportedHttpMethods {
INSTANCE;
/*
* (non-Javadoc)
* @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();
}
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.mapping.SupportedHttpMethods#supports(org.springframework.http.HttpMethod, org.springframework.data.rest.core.mapping.ResourceType)
*/
@Override
public boolean supports(HttpMethod method, ResourceType type) {
return false;
}
}
}

View File

@@ -1,133 +0,0 @@
/*
* Copyright 2014 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.invoke;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.AbstractIntegrationTests;
import org.springframework.data.rest.core.domain.jpa.Order;
import org.springframework.data.rest.core.domain.jpa.OrderRepository;
import org.springframework.data.rest.core.domain.jpa.Person;
import org.springframework.data.rest.core.domain.jpa.PersonRepository;
import org.springframework.format.support.DefaultFormattingConversionService;
/**
* Intgration tests for {@link CrudRepositoryInvoker}.
*
* @author Oliver Gierke
*/
public class CrudRepositoryInvokerIntegrationTests extends AbstractIntegrationTests {
@Autowired ApplicationContext context;
@Autowired PersonRepository personRepository;
@Autowired OrderRepository orderRepository;
/**
* @see DATAREST-216
*/
@Test
public void invokesRedeclaredSave() {
RepositoryInvoker invoker = getInvokerFor(orderRepository, OrderRepository.class);
Person person = personRepository.findOne(1L);
invoker.invokeSave(new Order(person));
}
/**
* @see DATAREST-216
*/
@Test
public void invokesRedeclaredFindOne() {
Person person = personRepository.findOne(1L);
Order order = orderRepository.save(new Order(person));
RepositoryInvoker invoker = getInvokerFor(orderRepository, OrderRepository.class);
invoker.invokeFindOne(order.getId());
}
/**
* @see DATAREST-216
*/
@Test
public void invokesDeleteOnCrudRepository() {
Person person = personRepository.findOne(1L);
Order order = orderRepository.save(new Order(person));
RepositoryInvoker invoker = getInvokerFor(orderRepository, CrudRepository.class);
invoker.invokeDelete(order.getId());
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private RepositoryInvoker getInvokerFor(Object repository, Class<?> expectedType) {
Object proxy = getVerifyingProxy(repository, expectedType);
Repositories repositories = new Repositories(context);
ConversionService conversionService = new DefaultFormattingConversionService();
return new CrudRepositoryInvoker((CrudRepository) proxy, repositories.getRepositoryInformationFor(Order.class),
conversionService);
}
@SuppressWarnings("unchecked")
private static <T> T getVerifyingProxy(T target, Class<?> expectedType) {
ProxyFactory factory = new ProxyFactory();
factory.setInterfaces(target.getClass().getInterfaces());
factory.setTarget(target);
factory.addAdvice(new VerifyingMethodInterceptor(expectedType));
return (T) factory.getProxy();
}
/**
* {@link MethodInterceptor} to verifiy the invocation was triggered on the given type.
*
* @author Oliver Gierke
*/
@SuppressWarnings("rawtypes")
private static final class VerifyingMethodInterceptor implements MethodInterceptor {
private final Class expectedInvocationTarget;
public VerifyingMethodInterceptor(Class<?> expectedInvocationTarget) {
this.expectedInvocationTarget = expectedInvocationTarget;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> type = invocation.getMethod().getDeclaringClass();
assertThat("Expected method invocation on " + expectedInvocationTarget + " but was invoked on " + type + "!",
type, is(equalTo(expectedInvocationTarget)));
return invocation.proceed();
}
}
}

View File

@@ -1,187 +0,0 @@
/*
* Copyright 2013 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.invoke;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import org.bson.types.ObjectId;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.AbstractIntegrationTests;
import org.springframework.data.rest.core.domain.jpa.Author;
import org.springframework.data.rest.core.domain.jpa.Order;
import org.springframework.data.rest.core.domain.jpa.OrderRepository;
import org.springframework.data.rest.core.domain.jpa.Person;
import org.springframework.data.rest.core.domain.jpa.PersonRepository;
/**
* Integration tests for {@link ReflectionRepositoryInvoker}.
*
* @author Oliver Gierke
*/
public class ReflectionRepositoryInvokerIntegrationTests extends AbstractIntegrationTests {
@Autowired Repositories repositories;
@Autowired ConversionService conversionService;
@Autowired PersonRepository repository;
@Autowired OrderRepository orderRepository;
@Autowired EntityManager em;
RepositoryInformation information;
RepositoryInvoker invoker;
@Before
public void setUp() {
information = repositories.getRepositoryInformationFor(Person.class);
invoker = new ReflectionRepositoryInvoker(repository, information, conversionService);
}
@Test
public void invokesFindOneWithStringIdCorrectly() {
Person person = repository.findAll().iterator().next();
assertThat(person, is(notNullValue()));
Object result = invoker.invokeFindOne(person.getId().toString());
assertThat(result, is(instanceOf(Person.class)));
}
@Test
public void invokesFindAllWithoutPageableCorrectly() {
Iterable<Object> result = invoker.invokeFindAll((Pageable) null);
assertThat(result, is(instanceOf(Page.class)));
}
@Test
public void invokesFindAllWithPageableCorrectly() {
Iterable<Object> result = invoker.invokeFindAll(new PageRequest(0, 10));
assertThat(result, is(instanceOf(Page.class)));
}
@Test
public void fallsBackToPlainFindAllIfRepositoryIsNotPaging() {
ReflectionRepositoryInvoker invoker = new ReflectionRepositoryInvoker(orderRepository,
repositories.getRepositoryInformationFor(Order.class), conversionService);
Iterable<Object> result = invoker.invokeFindAll(new PageRequest(0, 10));
assertThat(result, is(instanceOf(List.class)));
}
@Test
public void invokesQueryMethod() throws Exception {
HashMap<String, String[]> parameters = new HashMap<String, String[]>();
parameters.put("firstName", new String[] { "John" });
Method method = PersonRepository.class.getMethod("findByFirstName", String.class, Pageable.class);
Object result = invoker.invokeQueryMethod(method, parameters, null, null);
assertThat(result, is(instanceOf(Page.class)));
}
@Test
public void considersFormattingAnnotationsOnQueryMethodParameters() throws Exception {
HashMap<String, String[]> parameters = new HashMap<String, String[]>();
parameters.put("date", new String[] { "2013-07-18T10:49:00.000+02:00" });
Method method = PersonRepository.class.getMethod("findByCreatedUsingISO8601Date", Date.class, Pageable.class);
Object result = invoker.invokeQueryMethod(method, parameters, null, null);
assertThat(result, is(instanceOf(Page.class)));
Page<?> page = (Page<?>) result;
assertThat(page.getNumberOfElements(), is(1));
}
/**
* @see DATAREST-325
*/
@Test
public void invokesMethodOnPackageProtectedRepository() throws Exception {
Object authorRepository = repositories.getRepositoryFor(Author.class);
RepositoryInformation repositoryInformation = repositories.getRepositoryInformationFor(Author.class);
Method method = repositoryInformation.getRepositoryInterface().getMethod("findByFirstnameContaining", String.class);
Map<String, String[]> parameters = Collections.singletonMap("firstname", new String[] { "Oliver" });
ReflectionRepositoryInvoker invoker = new ReflectionRepositoryInvoker(authorRepository, information,
conversionService);
invoker.invokeQueryMethod(method, parameters, null, null);
}
/**
* @see DATAREST-335, DATAREST-346
*/
@Test
public void invokesOverriddenDeleteMethodCorrectly() {
MyRepo repository = mock(MyRepo.class);
MongoRepositoryFactoryBean<MyRepo, Domain, ObjectId> factory = new MongoRepositoryFactoryBean<MyRepo, Domain, ObjectId>();
factory.setMongoOperations(new MongoTemplate(mock(MongoDbFactory.class)));
factory.setRepositoryInterface(MyRepo.class);
factory.setLazyInit(true);
factory.afterPropertiesSet();
ObjectId id = new ObjectId();
ReflectionRepositoryInvoker invoker = new ReflectionRepositoryInvoker(repository,
factory.getRepositoryInformation(), conversionService);
// We must assume a non matching type here as clients might provide the raw ID value obtained from the request
invoker.invokeDelete(id.toString());
verify((CustomRepo) repository, times(1)).delete(id);
verify(repository, times(0)).findOne(Matchers.any(ObjectId.class));
}
interface MyRepo extends CustomRepo, CrudRepository<Domain, ObjectId> {}
interface Domain {}
interface CustomRepo {
void delete(ObjectId id);
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2014 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.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.rest.core.mapping.ResourceType.*;
import static org.springframework.http.HttpMethod.*;
import org.hamcrest.Matcher;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.CrudMethods;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.DefaultCrudMethods;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.http.HttpMethod;
/**
* Unit tests for {@link CrudMethodsSupportedHttpMethods}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class CrudMethodsSupportedHttpMethodsUnitTests {
/**
* @see DATACMNS-589, DATAREST-409
*/
@Test
public void doesNotSupportAnyHttpMethodForEmptyRepository() {
SupportedHttpMethods supportedMethods = getSupportedHttpMethodsFor(RawRepository.class);
assertMethodsSupported(supportedMethods, COLLECTION, true, OPTIONS);
assertMethodsSupported(supportedMethods, COLLECTION, false, GET, PUT, POST, PATCH, DELETE, HEAD);
assertMethodsSupported(supportedMethods, ITEM, true, OPTIONS);
assertMethodsSupported(supportedMethods, ITEM, false, GET, PUT, POST, PATCH, DELETE, HEAD);
}
/**
* @see DATAREST-217, DATAREST-330, DATACMNS-589, DATAREST-409
*/
@Test
public void defaultsSupportedHttpMethodsForItemResource() {
SupportedHttpMethods supportedHttpMethods = getSupportedHttpMethodsFor(SampleRepository.class);
assertMethodsSupported(supportedHttpMethods, ITEM, true, GET, PUT, PATCH, DELETE, OPTIONS, HEAD);
assertMethodsSupported(supportedHttpMethods, ITEM, false, POST);
}
/**
* @see DATAREST-217, DATAREST-330, DATACMNS-589, DATAREST-409
*/
@Test
public void defaultsSupportedHttpMethodsForCollectionResource() {
SupportedHttpMethods supportedHttpMethods = getSupportedHttpMethodsFor(SampleRepository.class);
assertMethodsSupported(supportedHttpMethods, COLLECTION, true, GET, POST, OPTIONS, HEAD);
assertMethodsSupported(supportedHttpMethods, COLLECTION, false, PUT, PATCH, DELETE);
}
/**
* @see DATACMNS-589, DATAREST-409
*/
@Test
public void doesNotSupportDeleteIfDeleteMethodIsNotExported() {
SupportedHttpMethods supportedHttpMethods = getSupportedHttpMethodsFor(HidesDelete.class);
assertMethodsSupported(supportedHttpMethods, ITEM, false, DELETE);
}
private static SupportedHttpMethods getSupportedHttpMethodsFor(Class<?> repositoryInterface) {
RepositoryMetadata metadata = new DefaultRepositoryMetadata(repositoryInterface);
CrudMethods crudMethods = new DefaultCrudMethods(metadata);
return new CrudMethodsSupportedHttpMethods(crudMethods);
}
private static void assertMethodsSupported(SupportedHttpMethods methods, ResourceType type, boolean supported,
HttpMethod... httpMethods) {
Matcher<Iterable<HttpMethod>> isSupported = supported ? hasItems(httpMethods) : not(hasItems(httpMethods));
assertThat(methods.getMethodsFor(type), isSupported);
for (HttpMethod method : httpMethods) {
assertThat(methods.supports(method, type), is(supported));
}
}
interface RawRepository extends Repository<Object, Long> {}
interface SampleRepository extends CrudRepository<Object, Long> {}
interface HidesDelete extends CrudRepository<Object, Long> {
@RestResource(exported = false)
void delete(Object id);
}
}

View File

@@ -30,6 +30,7 @@ import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.invoker.RepositoryInvoker;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.event.AfterCreateEvent;
@@ -38,9 +39,10 @@ import org.springframework.data.rest.core.event.AfterSaveEvent;
import org.springframework.data.rest.core.event.BeforeCreateEvent;
import org.springframework.data.rest.core.event.BeforeDeleteEvent;
import org.springframework.data.rest.core.event.BeforeSaveEvent;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.ResourceType;
import org.springframework.data.rest.core.mapping.SearchResourceMappings;
import org.springframework.data.rest.core.mapping.SupportedHttpMethods;
import org.springframework.data.rest.webmvc.support.BackendId;
import org.springframework.data.rest.webmvc.support.DefaultedPageable;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
@@ -113,7 +115,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
public ResponseEntity<?> optionsForCollectionResource(RootResourceInformation information) {
HttpHeaders headers = new HttpHeaders();
headers.setAllow(information.getSupportedMethods(ResourceType.COLLECTION));
SupportedHttpMethods supportedMethods = information.getSupportedMethods();
headers.setAllow(supportedMethods.getMethodsFor(ResourceType.COLLECTION));
return new ResponseEntity<Object>(headers, HttpStatus.OK);
}
@@ -243,7 +247,9 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
public ResponseEntity<?> optionsForItemResource(RootResourceInformation information) {
HttpHeaders headers = new HttpHeaders();
headers.setAllow(information.getSupportedMethods(ResourceType.ITEM));
SupportedHttpMethods supportedMethods = information.getSupportedMethods();
headers.setAllow(supportedMethods.getMethodsFor(ResourceType.ITEM));
headers.put("Accept-Patch", ACCEPT_PATCH_HEADERS);
return new ResponseEntity<Object>(headers, HttpStatus.OK);
@@ -449,12 +455,6 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem
resourceInformation.verifySupportedMethod(HttpMethod.GET, ResourceType.ITEM);
RepositoryInvoker repoMethodInvoker = resourceInformation.getInvoker();
if (!repoMethodInvoker.exposesFindOne()) {
throw new ResourceNotFoundException();
}
return repoMethodInvoker.invokeFindOne(id);
return resourceInformation.getInvoker().invokeFindOne(id);
}
}

View File

@@ -36,14 +36,15 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.BeanWrapper;
import org.springframework.data.repository.invoker.RepositoryInvoker;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.event.AfterLinkDeleteEvent;
import org.springframework.data.rest.core.event.AfterLinkSaveEvent;
import org.springframework.data.rest.core.event.BeforeLinkDeleteEvent;
import org.springframework.data.rest.core.event.BeforeLinkSaveEvent;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.rest.core.mapping.ResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.ResourceType;
import org.springframework.data.rest.core.util.Function;
import org.springframework.data.rest.webmvc.support.BackendId;
import org.springframework.data.web.PagedResourcesAssembler;
@@ -155,7 +156,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
final RepositoryInvoker repoMethodInvoker = repoRequest.getInvoker();
if (!repoMethodInvoker.exposesDelete()) {
// Can't delete a property if
if (repoRequest.getSupportedMethods().supports(HttpMethod.PUT, ResourceType.ITEM)) {
return new ResponseEntity<Resource<?>>(HttpStatus.METHOD_NOT_ALLOWED);
}
@@ -379,7 +381,8 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
final RepositoryInvoker invoker = repoRequest.getInvoker();
if (!invoker.exposesSave()) {
// Property can't be deleted if root resource can't be updated
if (!repoRequest.getSupportedMethods().supports(HttpMethod.PUT, ResourceType.ITEM)) {
throw new HttpRequestMethodNotSupportedException(HttpMethod.DELETE.name());
}
@@ -443,7 +446,7 @@ class RepositoryPropertyReferenceController extends AbstractRepositoryRestContro
RepositoryInvoker invoker = repoRequest.getInvoker();
if (!invoker.exposesFindOne()) {
if (!repoRequest.getSupportedMethods().supports(method, ResourceType.ITEM)) {
throw new HttpRequestMethodNotSupportedException(method.name());
}

View File

@@ -25,7 +25,7 @@ import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.repository.invoker.RepositoryInvoker;
import org.springframework.data.rest.core.mapping.MethodResourceMapping;
import org.springframework.data.rest.core.mapping.ParameterMetadata;
import org.springframework.data.rest.core.mapping.ResourceMappings;

View File

@@ -16,14 +16,15 @@
package org.springframework.data.rest.webmvc;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.repository.invoker.RepositoryInvoker;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.ResourceType;
import org.springframework.data.rest.core.mapping.SearchResourceMappings;
import org.springframework.data.rest.core.mapping.SupportedHttpMethods;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@@ -43,6 +44,7 @@ public class RootResourceInformation {
public RootResourceInformation(ResourceMetadata metadata, PersistentEntity<?, ?> entity, RepositoryInvoker invoker) {
this.resourceMetadata = metadata;
if (resourceMetadata == null || !resourceMetadata.isExported()) {
this.invoker = null;
@@ -74,75 +76,8 @@ public class RootResourceInformation {
return persistentEntity;
}
/**
* Returns the supported {@link HttpMethod}s for the given {@link ResourceType}.
*
* @param resourcType must not be {@literal null}.
* @return
*/
public Set<HttpMethod> getSupportedMethods(ResourceType resourcType) {
Assert.notNull(resourcType, "Resource type must not be null!");
if (invoker == null) {
return Collections.emptySet();
}
Set<HttpMethod> methods = new HashSet<HttpMethod>();
methods.add(HttpMethod.OPTIONS);
switch (resourcType) {
case COLLECTION:
if (invoker.exposesFindAll()) {
methods.add(HttpMethod.GET);
methods.add(HttpMethod.HEAD);
}
if (invoker.exposesSave()) {
methods.add(HttpMethod.POST);
}
break;
case ITEM:
if (invoker.exposesDelete() && invoker.hasFindOneMethod()) {
methods.add(HttpMethod.DELETE);
}
if (invoker.exposesFindOne()) {
methods.add(HttpMethod.GET);
methods.add(HttpMethod.HEAD);
}
if (invoker.exposesSave()) {
methods.add(HttpMethod.PUT);
methods.add(HttpMethod.PATCH);
}
break;
default:
throw new IllegalArgumentException(String.format("Unsupported resource type %s!", resourcType));
}
return Collections.unmodifiableSet(methods);
}
/**
* Returns whether the given {@link HttpMethod} is supported for the given {@link ResourceType}.
*
* @param httpMethod must not be {@literal null}.
* @param resourceType must not be {@literal null}.
* @return
*/
public boolean supports(HttpMethod httpMethod, ResourceType resourceType) {
Assert.notNull(httpMethod, "HTTP method must not be null!");
Assert.notNull(resourceType, "Resource type must not be null!");
return getSupportedMethods(resourceType).contains(httpMethod);
public SupportedHttpMethods getSupportedMethods() {
return resourceMetadata.getSupportedHttpMethods();
}
/**
@@ -164,7 +99,8 @@ public class RootResourceInformation {
Assert.notNull(httpMethod, "HTTP method must not be null!");
Assert.notNull(resourceType, "Resource type must not be null!");
Collection<HttpMethod> supportedMethods = getSupportedMethods(resourceType);
SupportedHttpMethods httpMethods = resourceMetadata.getSupportedHttpMethods();
Collection<HttpMethod> supportedMethods = httpMethods.getMethodsFor(resourceType);
if (!supportedMethods.contains(httpMethod)) {

View File

@@ -45,8 +45,9 @@ import org.springframework.data.rest.core.mapping.ResourceDescription;
import org.springframework.data.rest.core.mapping.ResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.mapping.ResourceType;
import org.springframework.data.rest.core.mapping.SimpleResourceDescription;
import org.springframework.data.rest.webmvc.ResourceType;
import org.springframework.data.rest.core.mapping.SupportedHttpMethods;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.data.rest.webmvc.json.JacksonMetadata;
import org.springframework.data.rest.webmvc.mapping.AssociationLinks;
@@ -121,14 +122,16 @@ public class RootResourceInformationToAlpsDescriptorConverter {
descriptors.add(representationDescriptor);
for (HttpMethod method : resourceInformation.getSupportedMethods(ResourceType.COLLECTION)) {
SupportedHttpMethods supportedHttpMethods = resourceInformation.getSupportedMethods();
for (HttpMethod method : supportedHttpMethods.getMethodsFor(ResourceType.COLLECTION)) {
if (!UNDOCUMENTED_METHODS.contains(method)) {
descriptors.add(buildCollectionResourceDescriptor(type, resourceInformation, representationDescriptor, method));
}
}
for (HttpMethod method : resourceInformation.getSupportedMethods(ResourceType.ITEM)) {
for (HttpMethod method : supportedHttpMethods.getMethodsFor(ResourceType.ITEM)) {
if (!UNDOCUMENTED_METHODS.contains(method)) {
descriptors.add(buildItemResourceDescriptor(resourceInformation, representationDescriptor, method));

View File

@@ -22,7 +22,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.repository.invoker.RepositoryInvoker;
import org.springframework.data.rest.webmvc.IncomingRequest;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;

View File

@@ -48,6 +48,8 @@ import org.springframework.data.geo.GeoModule;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.invoker.DefaultRepositoryInvokerFactory;
import org.springframework.data.repository.invoker.RepositoryInvokerFactory;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriToEntityConverter;
@@ -57,8 +59,6 @@ import org.springframework.data.rest.core.config.ProjectionDefinitionConfigurati
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
import org.springframework.data.rest.core.event.AnnotatedHandlerBeanPostProcessor;
import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener;
import org.springframework.data.rest.core.invoke.DefaultRepositoryInvokerFactory;
import org.springframework.data.rest.core.invoke.RepositoryInvokerFactory;
import org.springframework.data.rest.core.mapping.RepositoryResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceDescription;
import org.springframework.data.rest.core.mapping.ResourceMappings;
@@ -66,8 +66,8 @@ import org.springframework.data.rest.core.projection.ProxyProjectionFactory;
import org.springframework.data.rest.core.support.DomainObjectMerger;
import org.springframework.data.rest.core.support.RepositoryRelProvider;
import org.springframework.data.rest.core.util.UUIDConverter;
import org.springframework.data.rest.webmvc.BaseUriAwareController;
import org.springframework.data.rest.webmvc.BaseUri;
import org.springframework.data.rest.webmvc.BaseUriAwareController;
import org.springframework.data.rest.webmvc.BaseUriAwareHandlerMapping;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter;

View File

@@ -17,9 +17,9 @@ package org.springframework.data.rest.webmvc.config;
import org.springframework.core.MethodParameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.invoker.RepositoryInvoker;
import org.springframework.data.repository.invoker.RepositoryInvokerFactory;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.rest.core.invoke.RepositoryInvokerFactory;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.util.Assert;

View File

@@ -21,9 +21,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.repository.invoker.RepositoryInvokerFactory;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.Path;
import org.springframework.data.rest.core.invoke.RepositoryInvokerFactory;
import org.springframework.data.rest.core.mapping.ResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;

View File

@@ -20,6 +20,8 @@ import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.data.rest.core.mapping.ResourceType;
import org.springframework.data.rest.core.mapping.SupportedHttpMethods;
import org.springframework.data.rest.webmvc.jpa.Address;
import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig;
import org.springframework.http.HttpMethod;
@@ -43,8 +45,8 @@ public class RootResourceInformationIntegrationTests extends AbstractControllerI
@Test
public void getIsNotSupportedIfFindAllIsNotExported() {
RootResourceInformation information = getResourceInformation(Address.class);
assertThat(information.supports(HttpMethod.GET, ResourceType.COLLECTION), is(false));
SupportedHttpMethods supportedMethods = getResourceInformation(Address.class).getSupportedMethods();
assertThat(supportedMethods.supports(HttpMethod.GET, ResourceType.COLLECTION), is(false));
}
/**
@@ -53,7 +55,7 @@ public class RootResourceInformationIntegrationTests extends AbstractControllerI
@Test
public void postIsNotSupportedIfSaveIsNotExported() {
RootResourceInformation information = getResourceInformation(Address.class);
assertThat(information.supports(HttpMethod.POST, ResourceType.COLLECTION), is(false));
SupportedHttpMethods supportedMethods = getResourceInformation(Address.class).getSupportedMethods();
assertThat(supportedMethods.supports(HttpMethod.POST, ResourceType.COLLECTION), is(false));
}
}

View File

@@ -15,13 +15,10 @@
*/
package org.springframework.data.rest.webmvc;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.rest.webmvc.ResourceType.*;
import static org.springframework.data.rest.core.mapping.ResourceType.*;
import static org.springframework.http.HttpMethod.*;
import org.atteo.evo.inflector.English;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -31,7 +28,7 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.rest.core.invoke.RepositoryInvoker;
import org.springframework.data.repository.invoker.RepositoryInvoker;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@@ -57,90 +54,6 @@ public class RootResourceInformationUnitTests {
this.information = new RootResourceInformation(metadata, entity, invoker);
}
/**
* @see DATAREST-217, DATAREST-330
*/
@Test
public void defaultsSupportedHttpMethodsForItemResource() {
assertThat(information.getSupportedMethods(ResourceType.ITEM), hasItems(GET, PUT, PATCH, DELETE, OPTIONS));
assertThat(information.getSupportedMethods(ResourceType.ITEM), not(hasItems(POST)));
assertThat(information.getSupportedMethods(COLLECTION), hasItems(GET, POST, OPTIONS));
assertThat(information.getSupportedMethods(COLLECTION), not(hasItems(PUT, PATCH, DELETE)));
}
/**
* @see DATAREST-217
*/
@Test
public void doesNotSupportGetOnItemResourceIfFindOneIsNotExported() {
when(invoker.exposesFindOne()).thenReturn(false);
assertThat(information.supports(GET, ITEM), is(false));
}
/**
* @see DATAREST-217
*/
@Test
public void doesNotSupportDeleteOnItemResourceIfDeleteIsNotExported() {
when(invoker.exposesDelete()).thenReturn(false);
assertThat(information.supports(DELETE, ITEM), is(false));
}
/**
* @see DATAREST-217
*/
@Test
public void doesNotSupportPutOnItemResourceIfSaveIsNotExported() {
when(invoker.exposesSave()).thenReturn(false);
assertThat(information.supports(POST, ITEM), is(false));
}
/**
* @see DATAREST-330
*/
@Test
public void supportsHeadIfFindAllIsExposed() {
when(invoker.exposesFindAll()).thenReturn(true);
assertThat(information.supports(HEAD, COLLECTION), is(true));
}
/**
* @see DATAREST-330
*/
@Test
public void doesNotSupportHeadIfFindAllIsNotExposed() {
when(invoker.exposesFindAll()).thenReturn(false);
assertThat(information.supports(HEAD, COLLECTION), is(false));
}
/**
* @see DATAREST-330
*/
@Test
public void supportsHeadIfFindOneIsExposed() {
when(invoker.exposesFindOne()).thenReturn(true);
assertThat(information.supports(HEAD, ITEM), is(true));
}
/**
* @see DATAREST-330
*/
@Test
public void doesNotSupportHeadIfFindOneIsNotExposed() {
when(invoker.exposesFindOne()).thenReturn(false);
assertThat(information.supports(HEAD, ITEM), is(false));
}
/**
* @see DATAREST-330
*/