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,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);
}
}