From 2000d07b32c4c81fb45a43bbb30685ab52d2f770 Mon Sep 17 00:00:00 2001 From: Jon Brisbin Date: Mon, 20 Aug 2012 08:23:58 -0500 Subject: [PATCH] Introduced a MethodParameterConversionService to help convert query parameters that might be annotated with specific converters. --- .../repository/annotation/ConvertWith.java | 2 +- .../MethodParameterConversionService.java | 58 +++++++++++++++++++ .../rest/webmvc/RepositoryRestController.java | 51 ++++++---------- .../webmvc/StringToListOfLongsConverter.java | 9 ++- 4 files changed, 84 insertions(+), 36 deletions(-) create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/MethodParameterConversionService.java diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/ConvertWith.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/ConvertWith.java index a9e17f88a..a71a1dfb5 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/ConvertWith.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/annotation/ConvertWith.java @@ -16,6 +16,6 @@ import org.springframework.core.convert.converter.Converter; @Inherited public @interface ConvertWith { - Class> value(); + Class> value(); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/MethodParameterConversionService.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/MethodParameterConversionService.java new file mode 100644 index 000000000..aa4f10638 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/MethodParameterConversionService.java @@ -0,0 +1,58 @@ +package org.springframework.data.rest.repository.invoke; + +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.rest.repository.annotation.ConvertWith; +import org.springframework.util.Assert; + +/** + * A special conversion service that can convert {@link MethodParameter}s and their values to a target type, taking + * into account any specific conversion instructions annotated on the parameter with {@link ConvertWith}. + * + * @author Jon Brisbin + */ +public class MethodParameterConversionService { + + private final ConversionService delegateConversionService; + + public MethodParameterConversionService(ConversionService delegateConversionService) { + Assert.notNull(delegateConversionService, "Delegate ConversionService cannot be null."); + this.delegateConversionService = delegateConversionService; + } + + public boolean canConvert(Class sourceType, MethodParameter param) { + return canConvert(TypeDescriptor.valueOf(sourceType), param); + } + + public boolean canConvert(TypeDescriptor sourceType, MethodParameter param) { + return (delegateConversionService.canConvert(sourceType, new TypeDescriptor(param)) + || param.hasParameterAnnotation(ConvertWith.class)); + } + + @SuppressWarnings({"unchecked"}) + public T convert(Object source, MethodParameter param) { + return convert(source, TypeDescriptor.forObject(source), param); + } + + @SuppressWarnings({"unchecked"}) + public T convert(Object source, TypeDescriptor sourceType, MethodParameter param) { + TypeDescriptor targetType = new TypeDescriptor(param); + + try { + if(param.hasParameterAnnotation(ConvertWith.class)) { + Converter converter = (Converter)param.getParameterAnnotation(ConvertWith.class) + .value() + .newInstance(); + return converter.convert(source); + } else { + return (T)delegateConversionService.convert(source, sourceType, targetType); + } + } catch(Exception e) { + throw new ConversionFailedException(sourceType, targetType, source, e); + } + } + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java index 7735de639..50a913f59 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestController.java @@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; -import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; @@ -37,7 +36,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Page; @@ -64,7 +62,6 @@ import org.springframework.data.rest.repository.RepositoryExporterSupport; import org.springframework.data.rest.repository.RepositoryMetadata; import org.springframework.data.rest.repository.RepositoryNotFoundException; import org.springframework.data.rest.repository.UriToDomainObjectResolver; -import org.springframework.data.rest.repository.annotation.ConvertWith; import org.springframework.data.rest.repository.annotation.RestResource; import org.springframework.data.rest.repository.context.AfterDeleteEvent; import org.springframework.data.rest.repository.context.AfterLinkDeleteEvent; @@ -78,6 +75,7 @@ import org.springframework.data.rest.repository.context.BeforeRenderResourcesEve import org.springframework.data.rest.repository.context.BeforeSaveEvent; import org.springframework.data.rest.repository.context.RepositoryEvent; import org.springframework.data.rest.repository.invoke.CrudMethod; +import org.springframework.data.rest.repository.invoke.MethodParameterConversionService; import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.HttpHeaders; @@ -137,29 +135,33 @@ public class RepositoryRestController implements ApplicationContextAware, InitializingBean { - public static final String LOCATION = "Location"; - public static final String SELF = "self"; - final static ThreadLocal BASE_URI = new ThreadLocal(); - private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class); + public static final String LOCATION = "Location"; + public static final String SELF = "self"; + final static ThreadLocal BASE_URI = new ThreadLocal(); + private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class); + private static final TypeDescriptor STRING_ARRAY_TYPE = TypeDescriptor.valueOf(String[].class); /** * We manage a list of possible {@link ConversionService}s to handle converting objects in the controller. This list * is prioritized as well, so one can add a ConversionService at index 0 to make sure that ConversionService takes * priority whenever an object of the type it can convert is needing conversion. */ - private DelegatingConversionService conversionService = new DelegatingConversionService( + private DelegatingConversionService conversionService = new DelegatingConversionService( new DefaultFormattingConversionService() ); + private MethodParameterConversionService methodParameterConversionService = new MethodParameterConversionService( + conversionService + ); /** * Converters for reading and writing representations of objects. */ - private List httpMessageConverters = new ArrayList(); + private List httpMessageConverters = new ArrayList(); /** * List of {@link MediaType}s we can support, given the list of {@link HttpMessageConverter}s currently configured. */ - private SortedSet availableMediaTypes = new TreeSet(); - private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; - private ObjectMapper objectMapper = new ObjectMapper(); + private SortedSet availableMediaTypes = new TreeSet(); + private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; + private ObjectMapper objectMapper = new ObjectMapper(); private RepositoryAwareMappingHttpMessageConverter mappingHttpMessageConverter; private UriToDomainObjectResolver domainObjectResolver; private ApplicationContext applicationContext; @@ -576,8 +578,6 @@ public class RepositoryRestController return notFoundResponse(request); } - Annotation[][] annotations = queryMethod.method().getParameterAnnotations(); - Class[] paramTypes = queryMethod.paramTypes(); String[] paramNames = queryMethod.paramNames(); Object[] paramVals = new Object[paramTypes.length]; @@ -597,18 +597,7 @@ public class RepositoryRestController continue; } - TypeDescriptor stringTypeDesc = TypeDescriptor.valueOf(String[].class); MethodParameter methodParam = new MethodParameter(queryMethod.method(), i); - TypeDescriptor targetTypeDesc = new TypeDescriptor(methodParam); - - Class> converter = null; - for(Annotation anno : annotations[i]) { - if(ConvertWith.class.isAssignableFrom(anno.getClass())) { - converter = ((ConvertWith)anno).value(); - break; - } - } - String firstVal = (queryVals.length > 0 ? queryVals[0] : null); if(hasRepositoryMetadataFor(paramTypes[i])) { RepositoryMetadata paramRepoMeta = repositoryMetadataFor(paramTypes[i]); @@ -623,18 +612,12 @@ public class RepositoryRestController } paramVals[i] = o; - } else if(null != converter) { - try { - paramVals[i] = converter.newInstance().convert(queryVals); - } catch(InstantiationException e) { - throw new IllegalArgumentException(e); - } } else if(String.class.isAssignableFrom(paramTypes[i])) { // Param type is a String paramVals[i] = firstVal; - } else if(conversionService.canConvert(stringTypeDesc, targetTypeDesc)) { - // There's a converter from String -> param type - paramVals[i] = conversionService.convert(queryVals, stringTypeDesc, targetTypeDesc); + } else if(methodParameterConversionService.canConvert(STRING_ARRAY_TYPE, methodParam)) { + // There's a converter from String[] -> param type + paramVals[i] = methodParameterConversionService.convert(queryVals, STRING_ARRAY_TYPE, methodParam); } else { // Param type isn't a "simple" type or no converter exists, try JSON try { diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToListOfLongsConverter.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToListOfLongsConverter.java index 07fc30f22..83c632d02 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToListOfLongsConverter.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/StringToListOfLongsConverter.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import org.springframework.core.convert.converter.Converter; +import org.springframework.util.StringUtils; /** * @author Jon Brisbin @@ -12,7 +13,13 @@ public class StringToListOfLongsConverter implements Converter convert(String[] source) { List longs = new ArrayList(); - for(String s : source) { + String strings; + if(source.length == 1) { + strings = source[0]; + } else { + strings = StringUtils.arrayToCommaDelimitedString(source); + } + for(String s : StringUtils.commaDelimitedListToStringArray(strings)) { longs.add(Long.parseLong(s)); } return longs;