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