From 975b3337464eb28f6d0dfed7a2374b191b54fa89 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 17 Jul 2013 15:14:45 +0200 Subject: [PATCH] DATAREST-103 - Added support for non-CrudRepositor implementations. The exporter can now work with repositories that do not implement CrudRepository. Introduced a new RepositoryInvoker abstraction that transparently exposes which CRUD methods the repository offers. We still offer special invokers that can be used for CrudRepository and PagingaAndSortingRepository implementations to avoid the reflection overhead the general mechanism introduces. --- .../repository/UriDomainClassConverter.java | 2 +- .../invoke/CrudRepositoryInvoker.java | 145 +++------ .../PagingAndSortingRepositoryInvoker.java | 31 +- .../invoke/ReflectionRepositoryInvoker.java | 244 +++++++++++++++ .../RepositoryInvocationInformation.java | 10 +- .../repository/invoke/RepositoryInvoker.java | 19 +- .../invoke/RepositoryInvokerFactory.java | 47 +-- .../invoke/RepositoryMethodInvoker.java | 282 ------------------ ...tionRepositoryInvokerIntegrationTests.java | 82 +++++ .../AbstractRepositoryRestController.java | 2 +- .../webmvc/RepositoryEntityController.java | 131 +++----- ...RepositoryPropertyReferenceController.java | 56 ++-- .../rest/webmvc/RepositoryRestRequest.java | 41 ++- ...tRequestHandlerMethodArgumentResolver.java | 45 ++- .../webmvc/RepositorySearchController.java | 39 ++- .../RepositoryRestMvcConfiguration.java | 3 +- 16 files changed, 591 insertions(+), 588 deletions(-) create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/ReflectionRepositoryInvoker.java delete mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java create mode 100644 spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/ReflectionRepositoryInvokerIntegrationTests.java diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriDomainClassConverter.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriDomainClassConverter.java index 8ab44de72..748e4bfbb 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriDomainClassConverter.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/UriDomainClassConverter.java @@ -91,7 +91,7 @@ public class UriDomainClassConverter implements ConditionalGenericConverter { PersistentEntity entity = repositories.getPersistentEntity(targetType.getType()); - if (entity != null || !domainClassConverter.matches(STRING_TYPE, targetType)) { + if (entity == null || !domainClassConverter.matches(STRING_TYPE, targetType)) { throw new ConversionFailedException(sourceType, targetType, source, new IllegalArgumentException( "No PersistentEntity information available for " + targetType.getType())); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/CrudRepositoryInvoker.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/CrudRepositoryInvoker.java index a52ab8db7..8026cc664 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/CrudRepositoryInvoker.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/CrudRepositoryInvoker.java @@ -17,157 +17,88 @@ package org.springframework.data.rest.repository.invoke; import java.io.Serializable; -import org.springframework.data.domain.Page; +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.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 implements RepositoryInvoker { +class CrudRepositoryInvoker extends ReflectionRepositoryInvoker { private final CrudRepository repository; /** + * 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 repository) { + public CrudRepositoryInvoker(CrudRepository repository, RepositoryInformation information, + ConversionService conversionService) { + + super(repository, information, conversionService); this.repository = repository; } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) + /** + * Invokes the method equivalent to {@link CrudRepository#findAll()}. + * + * @return */ - @Override - public Iterable findAll(Sort sort) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Pageable) - */ - @Override - public Page findAll(Pageable pageable) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#save(java.lang.Object) - */ - @Override - public S save(S entity) { - return repository.save(entity); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) - */ - @Override - public Iterable save(Iterable entities) { - return repository.save(entities); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) - */ - @Override - public Object findOne(Serializable id) { - return repository.findOne(id); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) - */ - @Override - public boolean exists(Serializable id) { - return repository.exists(id); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#findAll() - */ - @Override - public Iterable findAll() { + protected Iterable invokeFindAll() { return repository.findAll(); } /* * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Sort) */ @Override - public Iterable findAll(Iterable ids) { - return repository.findAll(ids); + public Iterable invokeFindAll(Sort pageable) { + return repository.findAll(); } /* * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#count() + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Pageable) */ @Override - public long count() { - return repository.count(); + public Iterable invokeFindAll(Pageable pageable) { + return repository.findAll(); } - /* + /* * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeFindOne(java.io.Serializable) */ @Override - public void delete(Serializable id) { - repository.delete(id); + public Object invokeFindOne(Serializable id) { + return repository.findOne(convertId(id)); } - /* + /* * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) + * @see org.springframework.data.rest.repository.invoke.ReflectionRepositoryInvoker#invokeSave(java.lang.Object) */ @Override - public void delete(Object entity) { - repository.delete(entity); + public Object invokeSave(Object entity) { + return repository.save(entity); } - /* + /* * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeDelete(java.io.Serializable) */ @Override - public void delete(Iterable entities) { - repository.delete(entities); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.CrudRepository#deleteAll() - */ - @Override - public void deleteAll() { - repository.deleteAll(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.repository.invoke.RepositoryInvocationInformation#hasFindOne() - */ - @Override - public boolean hasFindOne() { - return true; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.rest.repository.invoke.RepositoryInvocationInformation#hasFindAll() - */ - @Override - public boolean hasFindAll() { - return true; + public void invokeDelete(Serializable id) { + repository.delete(convertId(id)); } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/PagingAndSortingRepositoryInvoker.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/PagingAndSortingRepositoryInvoker.java index 24ccd3ac1..acf196d2b 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/PagingAndSortingRepositoryInvoker.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/PagingAndSortingRepositoryInvoker.java @@ -17,12 +17,16 @@ package org.springframework.data.rest.repository.invoke; import java.io.Serializable; -import org.springframework.data.domain.Page; +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 { @@ -30,28 +34,35 @@ class PagingAndSortingRepositoryInvoker extends CrudRepositoryInvoker { private final PagingAndSortingRepository 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 repository) { - super(repository); + public PagingAndSortingRepositoryInvoker(PagingAndSortingRepository repository, + RepositoryInformation information, ConversionService conversionService) { + + super(repository, information, conversionService); this.repository = repository; } /* * (non-Javadoc) - * @see org.springframework.data.rest.repository.invoke.CrudRepositoryInvoker#findAll(org.springframework.data.domain.Pageable) + * @see org.springframework.data.rest.repository.invoke.CrudRepositoryInvoker#invokeFindAll(org.springframework.data.domain.Sort) */ @Override - public Page findAll(Pageable pageable) { - return repository.findAll(pageable); + public Iterable invokeFindAll(Sort sort) { + return sort == null ? invokeFindAll() : repository.findAll(sort); } - /* + /* * (non-Javadoc) - * @see org.springframework.data.rest.repository.invoke.CrudRepositoryInvoker#findAll(org.springframework.data.domain.Sort) + * @see org.springframework.data.rest.repository.invoke.CrudRepositoryInvoker#invokeFindAll(org.springframework.data.domain.Pageable) */ @Override - public Iterable findAll(Sort sort) { - return repository.findAll(sort); + public Iterable invokeFindAll(Pageable pageable) { + return pageable == null ? invokeFindAll() : repository.findAll(pageable); } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/ReflectionRepositoryInvoker.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/ReflectionRepositoryInvoker.java new file mode 100644 index 000000000..08237afe1 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/ReflectionRepositoryInvoker.java @@ -0,0 +1,244 @@ +/* + * 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.repository.invoke; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.support.ArgumentConvertingMethodInvoker; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionService; +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.rest.repository.annotation.RestResource; +import org.springframework.hateoas.core.MethodParameters; +import org.springframework.util.Assert; + +/** + * Base {@link RepositoryInvoker} using reflection to invoke methods on Spring Data Repositories. + * + * @author Oliver Gierke + */ +class ReflectionRepositoryInvoker implements RepositoryInvoker { + + 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.repository.invoke.RepositoryInvocationInformation#exposesFindAll() + */ + @Override + public boolean exposesFindAll() { + return methods.hasFindAllMethod() && exposes(methods.getFindAllMethod()); + } + + /* (non-Javadoc) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Sort) + */ + @Override + @SuppressWarnings("unchecked") + public Iterable invokeFindAll(Sort sort) { + return (Iterable) invoke(methods.getFindAllMethod(), sort); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeFindAll(org.springframework.data.domain.Pageable) + */ + @Override + @SuppressWarnings("unchecked") + public Iterable invokeFindAll(Pageable pageable) { + return (Iterable) invoke(methods.getFindAllMethod(), pageable); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvocationInformation#exposesSave() + */ + @Override + public boolean exposesSave() { + return methods.hasSaveMethod() && exposes(methods.getSaveMethod()); + } + + /* (non-Javadoc) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeSave(java.lang.Object) + */ + @Override + public Object invokeSave(Object object) { + return invoke(methods.getSaveMethod(), object); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvocationInformation#exposesFindOne() + */ + @Override + public boolean exposesFindOne() { + return methods.hasFindOneMethod() && exposes(methods.getFindOneMethod()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeFindOne(java.io.Serializable) + */ + @Override + public Object invokeFindOne(Serializable id) { + return invoke(methods.getFindOneMethod(), convertId(id)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvocationInformation#exposesDelete() + */ + @Override + public boolean exposesDelete() { + return methods.hasDelete() && exposes(methods.getDeleteMethod()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.repository.invoke.RepositoryInvoker#invokeDelete(java.io.Serializable) + */ + @Override + public void invokeDelete(Serializable id) { + + Method method = methods.getDeleteMethod(); + + if (method.getParameterTypes()[0].equals(Serializable.class)) { + 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.repository.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 parameters, Pageable pageable, Sort sort) { + return invoke(method, prepareParameters(method, parameters, pageable, sort)); + } + + private Object[] prepareParameters(Method method, Map rawParameters, Pageable pageable, Sort sort) { + + List parameters = new MethodParameters(method).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(); + String[] parameterValue = rawParameters.get(parameterName); + Object value = parameterValue.length == 1 ? parameterValue[0] : parameterValue; + + if (value == null) { + if (parameterName.startsWith("arg")) { + throw new IllegalArgumentException("No @Param annotation found on query method " + method.getName() + + " for parameter " + parameterName); + } else { + throw new IllegalArgumentException("No query parameter specified for " + method.getName() + " param '" + + parameterName + "'"); + } + } + + result[i] = conversionService.convert(parameterValue, targetType); + } + } + + return result; + } + + private Object invoke(Method method, Object... arguments) { + + BeanWrapperImpl wrapper = new BeanWrapperImpl(); + wrapper.setConversionService(conversionService); + + ArgumentConvertingMethodInvoker invoker = new ArgumentConvertingMethodInvoker(); + invoker.setTargetObject(repository); + invoker.setTargetMethod(method.getName()); + invoker.setArguments(arguments); + invoker.setTypeConverter(wrapper); + + try { + invoker.prepare(); + return invoker.invoke(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + /** + * 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()); + } +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvocationInformation.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvocationInformation.java index 7e12352c4..ffbbe7e63 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvocationInformation.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvocationInformation.java @@ -16,11 +16,17 @@ package org.springframework.data.rest.repository.invoke; /** + * Meta-information about the methods a repository exposes. + * * @author Oliver Gierke */ public interface RepositoryInvocationInformation { - boolean hasFindOne(); + boolean exposesSave(); - boolean hasFindAll(); + boolean exposesDelete(); + + boolean exposesFindOne(); + + boolean exposesFindAll(); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvoker.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvoker.java index 23ba8773f..eca880651 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvoker.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvoker.java @@ -16,13 +16,26 @@ package org.springframework.data.rest.repository.invoke; import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Map; -import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; /** * @author Oliver Gierke */ -public interface RepositoryInvoker extends PagingAndSortingRepository, - RepositoryInvocationInformation { +public interface RepositoryInvoker extends RepositoryInvocationInformation { + Object invokeSave(Object object); + + Object invokeFindOne(Serializable id); + + Iterable invokeFindAll(Pageable pageable); + + Iterable invokeFindAll(Sort pageable); + + void invokeDelete(Serializable serializable); + + Object invokeQueryMethod(Method method, Map parameters, Pageable pageable, Sort sort); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvokerFactory.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvokerFactory.java index 58a895406..cac431ab9 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvokerFactory.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryInvokerFactory.java @@ -19,8 +19,10 @@ 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; /** @@ -29,40 +31,49 @@ import org.springframework.data.repository.support.Repositories; public class RepositoryInvokerFactory { private final Repositories repositories; + private final ConversionService conversionService; + private final Map, RepositoryInvoker> invokers; /** * @param repositories - * @param invokers */ - public RepositoryInvokerFactory(Repositories repositories) { + public RepositoryInvokerFactory(Repositories repositories, ConversionService conversionService) { this.repositories = repositories; + this.conversionService = conversionService; this.invokers = new HashMap, RepositoryInvoker>(); - prepareInvokers(repositories); + } @SuppressWarnings("unchecked") - private final void prepareInvokers(Repositories repositories) { + private RepositoryInvoker prepareInvokers(Class domainType) { - for (Class domainType : repositories) { + Object repository = repositories.getRepositoryFor(domainType); + RepositoryInformation information = repositories.getRepositoryInformationFor(domainType); - Object repository = repositories.getRepositoryFor(domainType); - RepositoryInvoker invoker = null; - - if (repository instanceof PagingAndSortingRepository) { - invoker = new PagingAndSortingRepositoryInvoker((PagingAndSortingRepository) repository); - } else if (repository instanceof CrudRepository) { - invoker = new CrudRepositoryInvoker((CrudRepository) repository); - } else { - invoker = new RepositoryMethodInvoker(repository, null, null); - } - - invokers.put(domainType, invoker); + if (repository instanceof PagingAndSortingRepository) { + return new PagingAndSortingRepositoryInvoker((PagingAndSortingRepository) repository, + information, conversionService); + } else if (repository instanceof CrudRepository) { + return new CrudRepositoryInvoker((CrudRepository) repository, information, + conversionService); + } else { + return new ReflectionRepositoryInvoker(repository, information, conversionService); } } public RepositoryInvoker getInvokerFor(Class domainType) { - return invokers.get(domainType); + + RepositoryInvoker invoker = invokers.get(domainType); + + if (invoker != null) { + return invoker; + } + + invoker = prepareInvokers(domainType); + invokers.put(domainType, invoker); + + return invoker; } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java deleted file mode 100644 index abf85d55c..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodInvoker.java +++ /dev/null @@ -1,282 +0,0 @@ -package org.springframework.data.rest.repository.invoke; - -import static org.springframework.util.ReflectionUtils.*; - -import java.io.Serializable; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.core.MethodParameter; -import org.springframework.core.convert.ConversionService; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.rest.repository.support.ResourceMappingUtils; -import org.springframework.util.ReflectionUtils.MethodCallback; - -/** - * @author Jon Brisbin - */ -@SuppressWarnings("deprecation") -public class RepositoryMethodInvoker implements RepositoryInvoker { - - private final Object repository; - private final Map queryMethods = new HashMap(); - private final ConversionService conversionService; - - private RepositoryMethod saveOne; - private RepositoryMethod saveSome; - private RepositoryMethod findOne; - private RepositoryMethod exists; - private RepositoryMethod findAll; - private RepositoryMethod findAllSorted; - private RepositoryMethod findAllPaged; - private RepositoryMethod findSome; - private RepositoryMethod count; - private RepositoryMethod deleteOne; - private RepositoryMethod deleteOneById; - private RepositoryMethod deleteSome; - private RepositoryMethod deleteAll; - - public RepositoryMethodInvoker(Object repository, RepositoryInformation repoInfo, ConversionService conversionService) { - - this.repository = repository; - this.conversionService = conversionService; - Class repoType = repoInfo.getRepositoryInterface(); - - doWithMethods(repoType, new MethodCallback() { - @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - boolean exported = ResourceMappingUtils.findExported(method); - if (!exported) { - return; - } - String name = method.getName(); - int cardinality = method.getParameterTypes().length; - Class paramType = cardinality == 1 ? method.getParameterTypes()[0] : null; - boolean someMethod = null != paramType && Iterable.class.isAssignableFrom(paramType); - boolean byIdMethod = null != paramType && paramType == Serializable.class; - boolean sortable = null != paramType && Sort.class.isAssignableFrom(paramType); - boolean pageable = null != paramType && Pageable.class.isAssignableFrom(paramType); - RepositoryMethod repoMethod = new RepositoryMethod(method); - - if ("save".equals(name) && someMethod) { - saveSome = repoMethod; - } else if ("save".equals(name)) { - saveOne = repoMethod; - } else if ("findOne".equals(name)) { - findOne = repoMethod; - } else if ("exists".equals(name)) { - exists = repoMethod; - } else if ("findAll".equals(name) && sortable) { - findAllSorted = repoMethod; - } else if ("findAll".equals(name) && someMethod) { - findSome = repoMethod; - } else if ("findAll".equals(name) && pageable) { - findAllPaged = repoMethod; - } else if ("findAll".equals(name)) { - findAll = repoMethod; - } else if ("count".equals(name)) { - count = repoMethod; - } else if ("delete".equals(name) && byIdMethod) { - deleteOneById = repoMethod; - } else if ("delete".equals(name) && someMethod) { - deleteSome = repoMethod; - } else if ("delete".equals(name)) { - deleteOne = repoMethod; - } else if ("deleteAll".equals(name)) { - deleteAll = repoMethod; - } else { - queryMethods.put(name, repoMethod); - } - } - }); - } - - @SuppressWarnings({ "unchecked" }) - @Override - public S save(S entity) { - return (S) invokeMethod(saveOne.getMethod(), repository, entity); - } - - public boolean hasSaveOne() { - return null != saveOne; - } - - @SuppressWarnings({ "unchecked" }) - @Override - public Iterable save(Iterable entities) { - return (Iterable) invokeMethod(saveSome.getMethod(), repository, entities); - } - - public boolean hasSaveSome() { - return null != saveSome; - } - - @Override - public Object findOne(Serializable serializable) { - return invokeMethod(findOne.getMethod(), repository, serializable); - } - - public boolean hasFindOne() { - return null != findOne; - } - - @Override - public boolean exists(Serializable serializable) { - return (Boolean) invokeMethod(exists.getMethod(), repository, serializable); - } - - public boolean hasExists() { - return null != exists; - } - - @SuppressWarnings({ "unchecked" }) - @Override - public Iterable findAll() { - return (Iterable) invokeMethod(findAll.getMethod(), repository); - } - - public boolean hasFindAll() { - return null != findAll; - } - - @SuppressWarnings({ "unchecked" }) - @Override - public Iterable findAll(Iterable serializables) { - return (Iterable) invokeMethod(findSome.getMethod(), repository, serializables); - } - - public boolean hasFindSome() { - return null != findSome; - } - - @SuppressWarnings({ "unchecked" }) - @Override - public Iterable findAll(Sort sort) { - return (Iterable) invokeMethod(findAllSorted.getMethod(), repository, sort); - } - - public boolean hasFindAllSorted() { - return null != findAllSorted; - } - - @SuppressWarnings({ "unchecked" }) - @Override - public Page findAll(Pageable pageable) { - return (Page) invokeMethod(findAllPaged.getMethod(), repository, pageable); - } - - public boolean hasFindAllPageable() { - return null != findAllPaged; - } - - @Override - public void delete(Serializable serializable) { - invokeMethod(deleteOneById.getMethod(), repository, serializable); - } - - public boolean hasDeleteOneById() { - return null != deleteOneById; - } - - @Override - public long count() { - return (Long) invokeMethod(count.getMethod(), repository); - } - - public boolean hasCount() { - return null != count; - } - - @Override - public void delete(Object entity) { - invokeMethod(deleteOne.getMethod(), repository, entity); - } - - public boolean hasDeleteOne() { - return null != deleteOne; - } - - @Override - public void delete(Iterable entities) { - invokeMethod(deleteSome.getMethod(), repository, entities); - } - - public boolean hasDeleteSome() { - return null != deleteSome; - } - - @Override - public void deleteAll() { - invokeMethod(deleteAll.getMethod(), repository); - } - - public boolean hasDeleteAll() { - return null != deleteAll; - } - - public Map getQueryMethods() { - return queryMethods; - } - - public RepositoryMethod getRepositoryMethod(String name) { - return queryMethods.get(name); - } - - public Object invokeQueryMethod(String name, Object... params) { - RepositoryMethod repoMethod = queryMethods.get(name); - if (null == repoMethod) { - throw new NoSuchMethodError(name); - } - return invokeMethod(repoMethod.getMethod(), repository, params); - } - - public Object invokeQueryMethod(RepositoryMethod method, Object... params) { - return invokeMethod(method.getMethod(), repository, params); - } - - public Object invokeQueryMethod(RepositoryMethod method, Pageable pageable, Map rawParameters) { - return invokeQueryMethod(method, foo(method, pageable, rawParameters)); - } - - private Object[] foo(RepositoryMethod repoMethod, Pageable pageable, Map rawParameters) { - - List methodParams = repoMethod.getParameters(); - - if (methodParams.isEmpty()) { - return new Object[0]; - } - - Object[] paramValues = new Object[methodParams.size()]; - - for (int i = 0; i < paramValues.length; i++) { - MethodParameter param = methodParams.get(i); - Class targetType = param.getParameterType(); - if (Pageable.class.isAssignableFrom(targetType)) { - paramValues[i] = pageable; - } else if (Sort.class.isAssignableFrom(targetType)) { - paramValues[i] = pageable.getSort(); - } else { - String paramName = repoMethod.getParameterNames().get(i); - String[] queryParamVals = rawParameters.get(paramName); - if (null == queryParamVals) { - if (paramName.startsWith("arg")) { - throw new IllegalArgumentException("No @Param annotation found on query method " - + repoMethod.getMethod().getName() + " for parameter " + param.getParameterName()); - } else { - throw new IllegalArgumentException("No query parameter specified for " + repoMethod.getMethod().getName() - + " param '" + paramName + "'"); - } - } - paramValues[i] = conversionService.convert(queryParamVals, targetType); - } - } - - return paramValues; - } - -} diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/ReflectionRepositoryInvokerIntegrationTests.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/ReflectionRepositoryInvokerIntegrationTests.java new file mode 100644 index 000000000..08f3e4950 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/invoke/ReflectionRepositoryInvokerIntegrationTests.java @@ -0,0 +1,82 @@ +/* + * 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.repository.invoke; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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.repository.core.RepositoryInformation; +import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.repository.RepositoryTestsConfig; +import org.springframework.data.rest.repository.domain.jpa.Person; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * Integration tests for {@link ReflectionRepositoryInvoker}. + * + * @author Oliver Gierke + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = RepositoryTestsConfig.class) +public class ReflectionRepositoryInvokerIntegrationTests { + + @Autowired Repositories repositories; + @Autowired ConversionService conversionService; + + Object repository; + RepositoryInformation information; + RepositoryInvoker invoker; + + @Before + public void setUp() { + + information = repositories.getRepositoryInformationFor(Person.class); + repository = repositories.getRepositoryFor(Person.class); + invoker = new ReflectionRepositoryInvoker(repository, information, conversionService); + } + + @Test + public void invokesFindOneWithStringIdCorrectly() { + + Object result = invoker.invokeFindOne("1"); + assertThat(result, is(instanceOf(Person.class))); + } + + @Test + public void invokesFindAllWithoutPageableCorrectly() { + + Iterable result = invoker.invokeFindAll((Pageable) null); + assertThat(result, is(instanceOf(List.class))); + } + + @Test + public void invokesFindAllWithPageableCorrectly() { + + Iterable result = invoker.invokeFindAll(new PageRequest(0, 10)); + assertThat(result, is(instanceOf(Page.class))); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java index 32fbd5c0d..e671e292b 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/AbstractRepositoryRestController.java @@ -197,7 +197,7 @@ class AbstractRepositoryRestController implements MessageSourceAware, Initializi protected Link resourceLink(RepositoryRestRequest repoRequest, Resource resource) { - ResourceMetadata repoMapping = repoRequest.getRepositoryResourceMapping(); + ResourceMetadata repoMapping = repoRequest.getResourceMetadata(); Link selfLink = resource.getLink("self"); String rel = repoMapping.getSingleResourceRel(); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java index 5d22072bf..9123fcc15 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryEntityController.java @@ -17,7 +17,6 @@ package org.springframework.data.rest.webmvc; import static org.springframework.data.rest.webmvc.ControllerUtils.*; -import java.io.Serializable; import java.net.URI; import java.util.ArrayList; import java.util.Collections; @@ -30,6 +29,7 @@ import org.springframework.context.ApplicationEventPublisherAware; 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.mapping.PersistentProperty; import org.springframework.data.mapping.model.BeanWrapper; import org.springframework.data.repository.support.DomainClassConverter; @@ -42,7 +42,7 @@ import org.springframework.data.rest.repository.context.AfterSaveEvent; import org.springframework.data.rest.repository.context.BeforeCreateEvent; import org.springframework.data.rest.repository.context.BeforeDeleteEvent; import org.springframework.data.rest.repository.context.BeforeSaveEvent; -import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.repository.invoke.RepositoryInvoker; import org.springframework.data.rest.repository.mapping.ResourceMetadata; import org.springframework.data.rest.repository.mapping.SearchResourceMappings; import org.springframework.data.rest.repository.support.DomainObjectMerger; @@ -55,9 +55,6 @@ import org.springframework.hateoas.Resources; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; -import org.springframework.transaction.support.TransactionOperations; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -80,8 +77,6 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem private final ConversionService conversionService; private final DomainObjectMerger domainObjectMerger; - private final TransactionOperations txOperations; - private ApplicationEventPublisher publisher; @Autowired @@ -98,8 +93,6 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem this.converter = converter; this.conversionService = conversionService; this.domainObjectMerger = domainObjectMerger; - - this.txOperations = null; } /* @@ -114,33 +107,28 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/json", "application/x-spring-data-verbose+json" }) - public Resources listEntities(final RepositoryRestRequest request, Pageable pageable) + public Resources listEntities(final RepositoryRestRequest request, Pageable pageable, Sort sort) throws ResourceNotFoundException { List links = new ArrayList(); Iterable results; - RepositoryMethodInvoker repoMethodInvoker = request.getRepositoryMethodInvoker(); + RepositoryInvoker repoMethodInvoker = request.getRepositoryInvoker(); if (null == repoMethodInvoker) { throw new ResourceNotFoundException(); } - if (repoMethodInvoker.hasFindAllPageable()) { - results = repoMethodInvoker.findAll(pageable); - } else if (repoMethodInvoker.hasFindAllSorted()) { - results = repoMethodInvoker.findAll(pageable.getSort()); - } else if (repoMethodInvoker.hasFindAll()) { - results = repoMethodInvoker.findAll(); + if (pageable != null) { + results = repoMethodInvoker.invokeFindAll(pageable); } else { - throw new ResourceNotFoundException(); + results = repoMethodInvoker.invokeFindAll(sort); } - ResourceMetadata repoMapping = request.getRepositoryResourceMapping(); - - SearchResourceMappings searchMappings = repoMapping.getSearchResourceMappings(); + ResourceMetadata metadata = request.getResourceMetadata(); + SearchResourceMappings searchMappings = metadata.getSearchResourceMappings(); if (searchMappings.isExported()) { - links.add(entityLinks.linkFor(repoMapping.getDomainType()).slash(searchMappings.getPath()) + links.add(entityLinks.linkFor(metadata.getDomainType()).slash(searchMappings.getPath()) .withRel(searchMappings.getRel())); } @@ -149,13 +137,13 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem return resources; } + @ResponseBody @SuppressWarnings({ "unchecked" }) @RequestMapping(value = BASE_MAPPING, method = RequestMethod.GET, produces = { "application/x-spring-data-compact+json", "text/uri-list" }) - @ResponseBody - public Resources listEntitiesCompact(final RepositoryRestRequest repoRequest, Pageable pageable) - throws ResourceNotFoundException { - Resources resources = listEntities(repoRequest, pageable); + public Resources listEntitiesCompact(final RepositoryRestRequest repoRequest, Pageable pageable, Sort sort) { + + Resources resources = listEntities(repoRequest, pageable, sort); List links = new ArrayList(resources.getLinks()); for (Resource resource : ((Resources>) resources).getContent()) { @@ -169,18 +157,19 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem } } + @ResponseBody @RequestMapping(value = BASE_MAPPING, method = RequestMethod.POST, consumes = { "application/json" }, produces = { "application/json", "text/uri-list" }) - @ResponseBody public ResponseEntity> createNewEntity(RepositoryRestRequest repoRequest, PersistentEntityResource incoming) { - RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (null == repoMethodInvoker || !repoMethodInvoker.hasSaveOne()) { + + RepositoryInvoker invoker = repoRequest.getRepositoryInvoker(); + if (!invoker.exposesSave()) { throw new NoSuchMethodError(); } publisher.publishEvent(new BeforeCreateEvent(incoming.getContent())); - Object obj = repoMethodInvoker.save(incoming.getContent()); + Object obj = invoker.invokeSave(incoming.getContent()); publisher.publishEvent(new AfterCreateEvent(obj)); Link selfLink = perAssembler.getSelfLinkFor(obj); @@ -202,59 +191,58 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem * @return * @throws ResourceNotFoundException */ + @ResponseBody @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.GET, produces = { "application/json", "application/x-spring-data-compact+json", "text/uri-list" }) - @ResponseBody - public Resource getSingleEntity(RepositoryRestRequest repoRequest, @PathVariable String id) - throws ResourceNotFoundException { - RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (null == repoMethodInvoker || !repoMethodInvoker.hasFindOne()) { - throw new ResourceNotFoundException(); + public ResponseEntity> getSingleEntity(RepositoryRestRequest repoRequest, @PathVariable String id) { + + RepositoryInvoker repoMethodInvoker = repoRequest.getRepositoryInvoker(); + + if (!repoMethodInvoker.exposesFindOne()) { + return new ResponseEntity>(HttpStatus.NOT_FOUND); } - Object domainObj = converter.convert(id, STRING_TYPE, - TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType())); + Object domainObj = repoMethodInvoker.invokeFindOne(id); - if (null == domainObj) { - throw new ResourceNotFoundException(); + if (domainObj == null) { + return new ResponseEntity>(HttpStatus.NOT_FOUND); } - return perAssembler.toResource(domainObj); + return new ResponseEntity>(perAssembler.toResource(domainObj), HttpStatus.OK); } /** * {@code PUT / repository}/{id}} - Updates an existing entity or creates one at exactly that place. * - * @param repoRequest + * @param request * @param incoming * @param id * @return - * @throws ResourceNotFoundException */ + @ResponseBody @RequestMapping(value = BASE_MAPPING + "/{id}", method = RequestMethod.PUT, consumes = { "application/json" }, produces = { "application/json", "text/uri-list" }) - @ResponseBody - public ResponseEntity> updateEntity(RepositoryRestRequest repoRequest, - PersistentEntityResource incoming, @PathVariable String id) throws ResourceNotFoundException { + public ResponseEntity> updateEntity(RepositoryRestRequest request, + PersistentEntityResource incoming, @PathVariable String id) { - RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (null == repoMethodInvoker || !repoMethodInvoker.hasSaveOne() || !repoMethodInvoker.hasFindOne()) { - throw new NoSuchMethodError(); + RepositoryInvoker invoker = request.getRepositoryInvoker(); + if (!invoker.exposesSave() || !invoker.exposesFindOne()) { + return new ResponseEntity>(HttpStatus.METHOD_NOT_ALLOWED); } Object domainObj = converter.convert(id, STRING_TYPE, - TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType())); + TypeDescriptor.valueOf(request.getPersistentEntity().getType())); if (null == domainObj) { BeanWrapper incomingWrapper = BeanWrapper.create(incoming.getContent(), conversionService); PersistentProperty idProp = incoming.getPersistentEntity().getIdProperty(); incomingWrapper.setProperty(idProp, conversionService.convert(id, idProp.getType())); - return createNewEntity(repoRequest, incoming); + return createNewEntity(request, incoming); } domainObjectMerger.merge(incoming.getContent(), domainObj); publisher.publishEvent(new BeforeSaveEvent(incoming.getContent())); - Object obj = repoMethodInvoker.save(domainObj); + Object obj = invoker.invokeSave(domainObj); publisher.publishEvent(new AfterSaveEvent(obj)); if (config.isReturnBodyOnUpdate()) { @@ -268,10 +256,11 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem @ResponseBody public ResponseEntity deleteEntity(final RepositoryRestRequest repoRequest, @PathVariable final String id) throws ResourceNotFoundException, HttpRequestMethodNotSupportedException { - final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (null == repoMethodInvoker || !repoMethodInvoker.hasFindOne() - && !(repoMethodInvoker.hasDeleteOne() || repoMethodInvoker.hasDeleteOneById())) { - throw new HttpRequestMethodNotSupportedException("DELETE"); + + RepositoryInvoker invoker = repoRequest.getRepositoryInvoker(); + + if (!invoker.exposesDelete() || !invoker.exposesFindOne()) { + throw new HttpRequestMethodNotSupportedException(RequestMethod.DELETE.toString()); } // TODO: re-enable not exposing delete method if hidden @@ -281,36 +270,10 @@ class RepositoryEntityController extends AbstractRepositoryRestController implem // throw new HttpRequestMethodNotSupportedException("DELETE"); // } - final Object domainObj = converter.convert(id, STRING_TYPE, - TypeDescriptor.valueOf(repoRequest.getPersistentEntity().getType())); - if (null == domainObj) { - throw new ResourceNotFoundException(); - } + Object domainObj = invoker.invokeFindOne(id); publisher.publishEvent(new BeforeDeleteEvent(domainObj)); - TransactionCallbackWithoutResult callback = new TransactionCallbackWithoutResult() { - @Override - @SuppressWarnings({ "unchecked" }) - protected void doInTransactionWithoutResult(TransactionStatus status) { - if (repoMethodInvoker.hasDeleteOneById()) { - Class idType = (Class) repoRequest.getPersistentEntity() - .getIdProperty().getType(); - final Serializable idVal = conversionService.convert(id, idType); - repoMethodInvoker.delete(idVal); - } else if (repoMethodInvoker.hasDeleteOne()) { - repoMethodInvoker.delete(domainObj); - } - } - }; - - // FIXME - - if (txOperations != null) { - txOperations.execute(callback); - } else { - callback.doInTransaction(null); - } - + invoker.invokeDelete(id); publisher.publishEvent(new AfterDeleteEvent(domainObj)); return new ResponseEntity(HttpStatus.NO_CONTENT); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java index 3b296521f..333a965cb 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryPropertyReferenceController.java @@ -43,12 +43,13 @@ import org.springframework.data.rest.repository.context.AfterLinkDeleteEvent; import org.springframework.data.rest.repository.context.AfterLinkSaveEvent; import org.springframework.data.rest.repository.context.BeforeLinkDeleteEvent; import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent; -import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.repository.invoke.RepositoryInvoker; import org.springframework.data.rest.repository.mapping.ResourceMetadata; import org.springframework.data.web.PagedResourcesAssembler; import org.springframework.hateoas.Link; import org.springframework.hateoas.Resource; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -149,9 +150,11 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes public ResponseEntity> deletePropertyReference(final RepositoryRestRequest repoRequest, @PathVariable String id, @PathVariable String property) throws ResourceNotFoundException, NoSuchMethodException, HttpRequestMethodNotSupportedException { - final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (!repoMethodInvoker.hasDeleteOne()) { - throw new NoSuchMethodException(); + + final RepositoryInvoker repoMethodInvoker = repoRequest.getRepositoryInvoker(); + + if (!repoMethodInvoker.exposesDelete()) { + return new ResponseEntity>(HttpStatus.METHOD_NOT_ALLOWED); } Function> handler = new Function>() { @@ -169,7 +172,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } publisher.publishEvent(new BeforeLinkDeleteEvent(prop.wrapper.getBean(), prop.propertyValue)); - Object result = repoMethodInvoker.save(prop.wrapper.getBean()); + Object result = repoMethodInvoker.invokeSave(prop.wrapper.getBean()); publisher.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); return null; } @@ -245,7 +248,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes return response; } - ResourceMetadata repoMapping = repoRequest.getRepositoryResourceMapping(); + ResourceMetadata repoMapping = repoRequest.getResourceMetadata(); PersistentProperty persistentProp = repoRequest.getPersistentEntity().getPersistentProperty(property); Class propType = persistentProp.isCollectionLike() || persistentProp.isMap() ? persistentProp.getComponentType() @@ -281,17 +284,19 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes @ResponseBody public ResponseEntity> createPropertyReference(final RepositoryRestRequest repoRequest, final @RequestBody Resource incoming, @PathVariable String id, @PathVariable String property) - throws ResourceNotFoundException, NoSuchMethodException { - final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (!repoMethodInvoker.hasSaveOne()) { - throw new NoSuchMethodException(); + throws NoSuchMethodException { + + final RepositoryInvoker invoker = repoRequest.getRepositoryInvoker(); + + if (!invoker.exposesSave()) { + return new ResponseEntity>(HttpStatus.METHOD_NOT_ALLOWED); } Function> handler = new Function>() { @Override public Resource apply(ReferencedProperty prop) { if (prop.property.isCollectionLike()) { Collection coll = new ArrayList(); - if ("POST".equals(repoRequest.getRequest().getMethod())) { + if (HttpMethod.POST.equals(repoRequest.getRequestMethod())) { coll.addAll((Collection) prop.propertyValue); } for (Link l : incoming.getLinks()) { @@ -301,7 +306,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes prop.wrapper.setProperty(prop.property, coll); } else if (prop.property.isMap()) { Map m = new HashMap(); - if ("POST".equals(repoRequest.getRequest().getMethod())) { + if (HttpMethod.POST.equals(repoRequest.getRequestMethod())) { m.putAll((Map) prop.propertyValue); } for (Link l : incoming.getLinks()) { @@ -310,7 +315,7 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } prop.wrapper.setProperty(prop.property, m); } else { - if ("POST".equals(repoRequest.getRequest().getMethod())) { + if (HttpMethod.POST.equals(repoRequest.getRequestMethod())) { throw new IllegalStateException( "Cannot POST a reference to this singular property since the property type is not a List or a Map."); } @@ -323,12 +328,14 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } publisher.publishEvent(new BeforeLinkSaveEvent(prop.wrapper.getBean(), prop.propertyValue)); - Object result = repoMethodInvoker.save(prop.wrapper.getBean()); + Object result = invoker.invokeSave(prop.wrapper.getBean()); publisher.publishEvent(new AfterLinkSaveEvent(result, prop.propertyValue)); return null; } }; + doWithReferencedProperty(repoRequest, id, property, handler); + return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.CREATED); } @@ -336,10 +343,12 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes @ResponseBody public ResponseEntity> deletePropertyReferenceId(final RepositoryRestRequest repoRequest, @PathVariable String id, @PathVariable String property, final @PathVariable String propertyId) - throws ResourceNotFoundException, NoSuchMethodException { - final RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (!repoMethodInvoker.hasDeleteOne()) { - throw new NoSuchMethodException(); + throws NoSuchMethodException { + + final RepositoryInvoker invoker = repoRequest.getRepositoryInvoker(); + + if (!invoker.exposesDelete()) { + throw new NoSuchMethodError(); } Function> handler = new Function>() { @@ -373,11 +382,12 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } publisher.publishEvent(new BeforeLinkDeleteEvent(prop.wrapper.getBean(), prop.propertyValue)); - Object result = repoMethodInvoker.save(prop.wrapper.getBean()); + Object result = invoker.invokeSave(prop.wrapper.getBean()); publisher.publishEvent(new AfterLinkDeleteEvent(result, prop.propertyValue)); return null; } }; + doWithReferencedProperty(repoRequest, id, property, handler); return ControllerUtils.toResponseEntity(null, EMPTY_RESOURCE, HttpStatus.NO_CONTENT); @@ -395,9 +405,11 @@ public class RepositoryPropertyReferenceController extends AbstractRepositoryRes } private Resource doWithReferencedProperty(RepositoryRestRequest repoRequest, String id, String propertyPath, - Function> handler) throws ResourceNotFoundException, NoSuchMethodException { - RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); - if (!repoMethodInvoker.hasFindOne()) { + Function> handler) throws NoSuchMethodException { + + RepositoryInvoker repoMethodInvoker = repoRequest.getRepositoryInvoker(); + + if (!repoMethodInvoker.exposesFindOne()) { throw new NoSuchMethodException(); } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java index 1311d7581..2e796b2fb 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequest.java @@ -15,19 +15,15 @@ */ package org.springframework.data.rest.webmvc; -import java.io.Serializable; import java.net.URI; import javax.servlet.http.HttpServletRequest; -import org.springframework.core.convert.ConversionService; import org.springframework.data.mapping.PersistentEntity; -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.config.RepositoryRestConfiguration; -import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.repository.invoke.RepositoryInvoker; import org.springframework.data.rest.repository.mapping.ResourceMetadata; +import org.springframework.http.HttpMethod; +import org.springframework.web.context.request.NativeWebRequest; /** * @author Jon Brisbin @@ -35,48 +31,47 @@ import org.springframework.data.rest.repository.mapping.ResourceMetadata; */ class RepositoryRestRequest { - private final HttpServletRequest request; + private final NativeWebRequest request; private final URI baseUri; private final ResourceMetadata resourceMetadata; - private final RepositoryMethodInvoker repoMethodInvoker; + private final RepositoryInvoker repoInvoker; private final PersistentEntity persistentEntity; - public RepositoryRestRequest(RepositoryRestConfiguration config, Repositories repositories, - HttpServletRequest request, URI baseUri, ResourceMetadata repoInfo, ConversionService conversionService) { + public RepositoryRestRequest(PersistentEntity entity, NativeWebRequest request, URI baseUri, + ResourceMetadata repoInfo, RepositoryInvoker invoker) { this.request = request; this.baseUri = baseUri; this.resourceMetadata = repoInfo; if (resourceMetadata == null || !resourceMetadata.isExported()) { - this.repoMethodInvoker = null; + this.repoInvoker = null; this.persistentEntity = null; } else { - - Class domainType = repoInfo.getDomainType(); - CrudRepository repositoryFor = repositories.getRepositoryFor(domainType); - RepositoryInformation information = repositories.getRepositoryInformationFor(domainType); - - this.repoMethodInvoker = new RepositoryMethodInvoker(repositoryFor, information, conversionService); - this.persistentEntity = repositories.getPersistentEntity(domainType); + this.repoInvoker = invoker; + this.persistentEntity = entity; } } - HttpServletRequest getRequest() { + NativeWebRequest getRequest() { return request; } + HttpMethod getRequestMethod() { + return HttpMethod.valueOf(request.getNativeRequest(HttpServletRequest.class).getMethod()); + } + URI getBaseUri() { return baseUri; } - ResourceMetadata getRepositoryResourceMapping() { + ResourceMetadata getResourceMetadata() { return resourceMetadata; } - RepositoryMethodInvoker getRepositoryMethodInvoker() { - return repoMethodInvoker; + RepositoryInvoker getRepositoryInvoker() { + return repoInvoker; } PersistentEntity getPersistentEntity() { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java index 9a06081b2..eb77f7ca7 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestRequestHandlerMethodArgumentResolver.java @@ -17,14 +17,14 @@ package org.springframework.data.rest.webmvc; import java.net.URI; -import javax.servlet.http.HttpServletRequest; - -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.repository.support.Repositories; -import org.springframework.data.rest.config.RepositoryRestConfiguration; +import org.springframework.data.rest.repository.invoke.RepositoryInvoker; +import org.springframework.data.rest.repository.invoke.RepositoryInvokerFactory; import org.springframework.data.rest.repository.mapping.ResourceMetadata; +import org.springframework.util.Assert; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -36,15 +36,29 @@ import org.springframework.web.method.support.ModelAndViewContainer; */ public class RepositoryRestRequestHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { - private final ConversionService conversionService; + private final Repositories repositories; + private final RepositoryInvokerFactory invokerFactory; + private final ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver; + private final BaseUriMethodArgumentResolver baseUriResolver; - @Autowired private RepositoryRestConfiguration config; - @Autowired private Repositories repositories; - @Autowired private ResourceMetadataHandlerMethodArgumentResolver repoInfoResolver; - @Autowired private BaseUriMethodArgumentResolver baseUriResolver; + /** + * Creates a new {@link RepositoryRestRequestHandlerMethodArgumentResolver} using the given {@link Repositories} and + * {@link ConversionService}. + * + * @param repositories must not be {@literal null}. + * @param conversionService must not be {@literal null}. + */ + public RepositoryRestRequestHandlerMethodArgumentResolver(Repositories repositories, + ConversionService conversionService, ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver, + BaseUriMethodArgumentResolver baseUriResolver) { - public RepositoryRestRequestHandlerMethodArgumentResolver(ConversionService conversionService) { - this.conversionService = conversionService; + Assert.notNull(repositories, "Repositories must not be null!"); + Assert.notNull(conversionService, "ConversionService must not be null!"); + + this.repositories = repositories; + this.invokerFactory = new RepositoryInvokerFactory(repositories, conversionService); + this.resourceMetadataResolver = resourceMetadataResolver; + this.baseUriResolver = baseUriResolver; } /* @@ -65,11 +79,14 @@ public class RepositoryRestRequestHandlerMethodArgumentResolver implements Handl NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { URI baseUri = baseUriResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); - ResourceMetadata repoInfo = repoInfoResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); + ResourceMetadata repoInfo = resourceMetadataResolver.resolveArgument(parameter, mavContainer, webRequest, + binderFactory); + + RepositoryInvoker repositoryInvoker = invokerFactory.getInvokerFor(repoInfo.getDomainType()); + PersistentEntity persistentEntity = repositories.getPersistentEntity(repoInfo.getDomainType()); // TODO reject if ResourceMetadata cannot be resolved - return new RepositoryRestRequest(config, repositories, webRequest.getNativeRequest(HttpServletRequest.class), - baseUri, repoInfo, conversionService); + return new RepositoryRestRequest(persistentEntity, webRequest, baseUri, repoInfo, repositoryInvoker); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java index b119f00a8..4a1c1304d 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositorySearchController.java @@ -26,8 +26,7 @@ import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Pageable; -import org.springframework.data.rest.repository.invoke.RepositoryMethod; -import org.springframework.data.rest.repository.invoke.RepositoryMethodInvoker; +import org.springframework.data.rest.repository.invoke.RepositoryInvoker; import org.springframework.data.rest.repository.mapping.ResourceMapping; import org.springframework.data.rest.repository.mapping.ResourceMappings; import org.springframework.data.rest.repository.mapping.ResourceMetadata; @@ -39,6 +38,8 @@ import org.springframework.hateoas.LinkBuilder; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.Resources; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -98,40 +99,39 @@ class RepositorySearchController extends AbstractRepositoryRestController { @RequestMapping(value = BASE_MAPPING + "/{method}", method = RequestMethod.GET, produces = { "application/json", "application/x-spring-data-verbose+json" }) @ResponseBody - public ResourceSupport query(final RepositoryRestRequest repoRequest, @PathVariable String repository, - @PathVariable String method, Pageable pageable) throws ResourceNotFoundException { + public ResponseEntity query(final RepositoryRestRequest repoRequest, + @PathVariable String repository, @PathVariable String method, Pageable pageable) throws ResourceNotFoundException { - RepositoryMethodInvoker repoMethodInvoker = repoRequest.getRepositoryMethodInvoker(); + ResourceMetadata metadata = repoRequest.getResourceMetadata(); + SearchResourceMappings searchMapping = metadata.getSearchResourceMappings(); - if (repoMethodInvoker.getQueryMethods().isEmpty()) { - throw new ResourceNotFoundException(); + if (searchMapping.isExported()) { + return new ResponseEntity(HttpStatus.NOT_FOUND); } - ResourceMetadata repoMapping = repoRequest.getRepositoryResourceMapping(); + RepositoryInvoker repoMethodInvoker = repoRequest.getRepositoryInvoker(); - SearchResourceMappings searchResourceMappings = repoMapping.getSearchResourceMappings(); - Method mappedMethod = searchResourceMappings.getMappedMethod(method); + Method mappedMethod = searchMapping.getMappedMethod(method); if (mappedMethod == null) { - throw new ResourceNotFoundException(); + return new ResponseEntity(HttpStatus.NOT_FOUND); } - RepositoryMethod repositoryMethod = new RepositoryMethod(mappedMethod); + Map parameters = repoRequest.getRequest().getParameterMap(); + Object result = repoMethodInvoker.invokeQueryMethod(mappedMethod, parameters, pageable, null); - Map rawParameters = repoRequest.getRequest().getParameterMap(); - Object result = repoMethodInvoker.invokeQueryMethod(repositoryMethod, pageable, rawParameters); - - return resultToResources(result); + return new ResponseEntity(resultToResources(result), HttpStatus.OK); } + @ResponseBody @RequestMapping(value = BASE_MAPPING + "/{method}", method = RequestMethod.GET, produces = { "application/x-spring-data-compact+json" }) - @ResponseBody public ResourceSupport queryCompact(RepositoryRestRequest repoRequest, @PathVariable String repository, - @PathVariable String method, Pageable pageable) throws ResourceNotFoundException { + @PathVariable String method, Pageable pageable) { List links = new ArrayList(); - ResourceSupport resource = query(repoRequest, repository, method, pageable); + ResponseEntity entity = query(repoRequest, repository, method, pageable); + ResourceSupport resource = entity.getBody(); links.addAll(resource.getLinks()); if (resource instanceof Resources && ((Resources) resource).getContent() != null) { @@ -148,5 +148,4 @@ class RepositorySearchController extends AbstractRepositoryRestController { return new Resource(EMPTY_RESOURCE_LIST, links); } - } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 420f36bd9..34640b6c5 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -213,7 +213,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon */ @Bean public RepositoryRestRequestHandlerMethodArgumentResolver repoRequestArgumentResolver() { - return new RepositoryRestRequestHandlerMethodArgumentResolver(defaultConversionService()); + return new RepositoryRestRequestHandlerMethodArgumentResolver(repositories(), defaultConversionService(), + resourceMetadataHandlerMethodArgumentResolver(), baseUriMethodArgumentResolver()); } @Bean