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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user