From 7f8abc5aa835d45713a1124a842eeb985d9c12c1 Mon Sep 17 00:00:00 2001 From: Jon Brisbin Date: Thu, 26 Jul 2012 13:31:33 -0500 Subject: [PATCH] Big update. Bug fixes, code re-formatting, changing tests. --- build.gradle | 4 +- gradle.properties | 2 +- .../data/rest/core/Handler.java | 4 +- .../springframework/data/rest/core/Links.java | 25 + .../data/rest/core/SimpleLink.java | 5 +- .../convert/DelegatingConversionService.java | 45 +- .../core/convert/StringToUUIDConverter.java | 3 +- .../core/convert/UUIDToStringConverter.java | 3 +- .../data/rest/core/util/BeanUtils.java | 96 +- .../core/util/FluentBeanDeserializer.java | 31 +- .../rest/core/util/FluentBeanSerializer.java | 54 +- .../data/rest/core/util/FluentBeanUtils.java | 70 +- .../data/rest/core/util/UriUtils.java | 76 +- spring-data-rest-repository/build.gradle | 7 +- .../rest/repository/AttributeMetadata.java | 23 +- .../data/rest/repository/EntityMetadata.java | 4 +- ...epositoryConstraintViolationException.java | 3 +- .../rest/repository/RepositoryExporter.java | 78 +- .../repository/RepositoryExporterSupport.java | 34 +- .../rest/repository/RepositoryMetadata.java | 10 +- .../RepositoryNotFoundException.java | 3 +- .../repository/RepositoryQueryMethod.java | 56 - .../rest/repository/ValidationErrors.java | 9 +- .../AbstractRepositoryEventListener.java | 37 +- .../repository/context/AfterDeleteEvent.java | 3 +- .../context/AfterLinkSaveEvent.java | 3 +- .../repository/context/AfterSaveEvent.java | 3 +- ...notatedHandlerRepositoryEventListener.java | 61 +- .../repository/context/BeforeDeleteEvent.java | 3 +- .../context/BeforeLinkSaveEvent.java | 3 +- .../repository/context/BeforeSaveEvent.java | 3 +- .../repository/context/LinkSaveEvent.java | 3 +- .../repository/context/RepositoryEvent.java | 3 +- .../ValidatingRepositoryEventListener.java | 40 +- .../{ => invoke}/RepositoryMethod.java | 51 +- .../invoke/RepositoryMethodResponse.java | 103 + .../invoke/RepositoryQueryMethod.java | 64 + .../repository/jpa/JpaAttributeMetadata.java | 57 +- .../repository/jpa/JpaEntityMetadata.java | 35 +- .../repository/jpa/JpaRepositoryExporter.java | 3 +- .../repository/jpa/JpaRepositoryMetadata.java | 23 +- .../repository/spec/JpaMetadataSpec.groovy | 9 +- .../repository/test/ApplicationConfig.java | 66 + spring-data-rest-webmvc/build.gradle | 10 +- .../data/rest/webmvc/JacksonUtil.java | 44 +- .../data/rest/webmvc/Links.java | 28 - .../data/rest/webmvc/MediaTypes.java | 33 + .../data/rest/webmvc/PagingAndSorting.java | 20 +- ...agingAndSortingMethodArgumentResolver.java | 35 +- .../webmvc/RepositoryRestConfiguration.java | 39 +- .../rest/webmvc/RepositoryRestController.java | 1964 ++++++++++------- .../webmvc/RepositoryRestHandlerAdapter.java | 18 +- .../webmvc/RepositoryRestHandlerMapping.java | 15 +- .../RepositoryRestMvcConfiguration.java | 9 +- ...rverHttpRequestMethodArgumentResolver.java | 15 +- .../webmvc/UriListHttpMessageConverter.java | 93 +- .../src/main/webapp/WEB-INF/web.xml | 18 - .../data/rest/webmvc/spec/BaseSpec.groovy | 106 + .../data/rest/webmvc/spec/EventsSpec.groovy | 47 + .../rest/webmvc/spec/RelationshipsSpec.groovy | 40 + .../spec/RepositoryRestControllerSpec.groovy | 2 + .../webmvc/spec/TopLevelEntitySpec.groovy | 39 + .../data/rest/test/RestBuilder.java | 40 +- .../data/rest/test/webmvc/Address.java | 21 +- .../rest/test/webmvc/AddressRepository.java | 4 + .../rest/test/webmvc/ApplicationConfig.java | 64 + .../data/rest/test/webmvc/Family.java | 6 +- .../rest/test/webmvc/FamilyRepository.java | 3 +- .../data/rest/test/webmvc/Person.java | 14 +- .../data/rest/test/webmvc/PersonLoader.java | 8 +- .../rest/test/webmvc/PersonRepository.java | 1 - .../rest/test/webmvc/PersonValidator.java | 5 +- .../data/rest/test/webmvc/Profile.java | 27 +- .../rest/test/webmvc/ProfileRepository.java | 4 + .../webmvc/RestExporterWebInitializer.java | 33 + .../webmvc/TestRepositoryEventListener.java | 32 + .../rest/test/webmvc/UuidTestRepository.java | 3 +- .../spring-data-rest/repositories-export.xml | 8 +- .../META-INF/spring-data-rest/shared.xml | 25 - .../src/test/resources/load_data.sh | 1 + 80 files changed, 2551 insertions(+), 1541 deletions(-) create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java delete mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryQueryMethod.java rename spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/{ => invoke}/RepositoryMethod.java (63%) create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java create mode 100644 spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryQueryMethod.java create mode 100644 spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java delete mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/Links.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/MediaTypes.java delete mode 100644 spring-data-rest-webmvc/src/main/webapp/WEB-INF/web.xml create mode 100644 spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/BaseSpec.groovy create mode 100644 spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy create mode 100644 spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RelationshipsSpec.groovy create mode 100644 spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/TopLevelEntitySpec.groovy create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ApplicationConfig.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/RestExporterWebInitializer.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/TestRepositoryEventListener.java delete mode 100644 spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/shared.xml diff --git a/build.gradle b/build.gradle index daa266efd..b65166963 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ allprojects { repositories { //maven { url "http://repo.springsource.org/libs-snapshot" } - //maven { url "http://repo.springsource.org/libs-milestone" } + maven { url "http://repo.springsource.org/libs-milestone" } maven { url "http://repo.springsource.org/libs-release" } } @@ -70,7 +70,9 @@ configure(subprojects) { subproject -> compile("org.springframework:spring-context:$springVersion") { force = true } compile("org.springframework:spring-core:$springVersion") { force = true } compile("org.springframework:spring-orm:$springVersion") { force = true } + compile("org.springframework:spring-tx:$springVersion") { force = true } compile("org.springframework:spring-web:$springVersion") { force = true } + runtime "cglib:cglib-nodep:2.2.2" // Testing testCompile "org.spockframework:spock-core:$spockVersion" diff --git a/gradle.properties b/gradle.properties index fc09ec164..16a40318d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ groovyVersion = 1.8.6 # Supporting libraries sdCommonsVersion = 1.3.2.RELEASE -sdJpaVersion = 1.1.0.RELEASE +sdJpaVersion = 1.2.0.M1 jacksonVersion = 1.9.7 hibernateVersion = 4.1.4.Final diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Handler.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Handler.java index 22c5e1eb3..2d4e0566d 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Handler.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Handler.java @@ -10,7 +10,9 @@ public interface Handler { /** * Accept an argument and possibly produce a result. * - * @param t arg + * @param t + * arg + * * @return Some object or {@literal null} if no result. */ V handle(T t); diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java new file mode 100644 index 000000000..980995cab --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/Links.java @@ -0,0 +1,25 @@ +package org.springframework.data.rest.core; + +import java.util.ArrayList; +import java.util.List; + +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * @author Jon Brisbin + */ +public class Links { + + private List links = new ArrayList(); + + public Links add(Link link) { + links.add(link); + return this; + } + + @JsonProperty("_links") + public List getLinks() { + return this.links; + } + +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/SimpleLink.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/SimpleLink.java index 558594f3e..2eac65899 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/SimpleLink.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/SimpleLink.java @@ -7,10 +7,11 @@ import java.net.URI; * * @author Jon Brisbin */ -public class SimpleLink implements Link { +public class SimpleLink + implements Link { private String rel; - private URI href; + private URI href; public SimpleLink() { } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/DelegatingConversionService.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/DelegatingConversionService.java index fc93c7f62..83c2c20bc 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/DelegatingConversionService.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/DelegatingConversionService.java @@ -7,9 +7,14 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; /** - * @author Jon Brisbin + * This {@link ConversionService} implementation delegates the actual conversion the ConversionService if finds in its + * internal List that claims to be able to convert a given class. It will roll through the internal Stack + * of ConversionServices until it finds one that can convert the given type. + * + * @author Jon Brisbin */ -public class DelegatingConversionService implements ConversionService { +public class DelegatingConversionService + implements ConversionService { private Stack conversionServices = new Stack(); @@ -20,21 +25,39 @@ public class DelegatingConversionService implements ConversionService { addConversionServices(svcs); } + /** + * Add {@link ConversionService}s to the internal list of those to delegate to. + * + * @param svcs + * The ConversionServices to delegate to (in order). + * + * @return @this + */ public DelegatingConversionService addConversionServices(ConversionService... svcs) { - for (ConversionService svc : svcs) { + for(ConversionService svc : svcs) { conversionServices.add(svc); } return this; } + /** + * Add a {@link ConversionService} to the internal list at a specific index for controlling the priority. + * + * @param atIndex + * Where in the stack to add this ConversionService. + * @param svc + * The ConversionService to add. + * + * @return + */ public DelegatingConversionService addConversionService(int atIndex, ConversionService svc) { conversionServices.add(atIndex, svc); return this; } @Override public boolean canConvert(Class from, Class to) { - for (ConversionService svc : conversionServices) { - if (svc.canConvert(from, to)) { + for(ConversionService svc : conversionServices) { + if(svc.canConvert(from, to)) { return true; } } @@ -42,8 +65,8 @@ public class DelegatingConversionService implements ConversionService { } @Override public boolean canConvert(TypeDescriptor from, TypeDescriptor to) { - for (ConversionService svc : conversionServices) { - if (svc.canConvert(from, to)) { + for(ConversionService svc : conversionServices) { + if(svc.canConvert(from, to)) { return true; } } @@ -51,8 +74,8 @@ public class DelegatingConversionService implements ConversionService { } @Override public T convert(Object o, Class type) { - for (ConversionService svc : conversionServices) { - if (svc.canConvert(o.getClass(), type)) { + for(ConversionService svc : conversionServices) { + if(svc.canConvert(o.getClass(), type)) { return svc.convert(o, type); } } @@ -60,8 +83,8 @@ public class DelegatingConversionService implements ConversionService { } @Override public Object convert(Object o, TypeDescriptor from, TypeDescriptor to) { - for (ConversionService svc : conversionServices) { - if (svc.canConvert(from, to)) { + for(ConversionService svc : conversionServices) { + if(svc.canConvert(from, to)) { return svc.convert(o, from, to); } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/StringToUUIDConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/StringToUUIDConverter.java index 77bfb752c..d90c91d5f 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/StringToUUIDConverter.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/StringToUUIDConverter.java @@ -7,7 +7,8 @@ import org.springframework.core.convert.converter.Converter; /** * @author Jon Brisbin */ -public class StringToUUIDConverter implements Converter { +public class StringToUUIDConverter + implements Converter { @Override public UUID convert(String s) { return (null != s ? UUID.fromString(s) : null); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/UUIDToStringConverter.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/UUIDToStringConverter.java index 84933a6e3..51df846ab 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/UUIDToStringConverter.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/convert/UUIDToStringConverter.java @@ -7,7 +7,8 @@ import org.springframework.core.convert.converter.Converter; /** * @author Jon Brisbin */ -public class UUIDToStringConverter implements Converter { +public class UUIDToStringConverter + implements Converter { @Override public String convert(UUID uuid) { return (null != uuid ? uuid.toString() : null); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java index b219536ad..d3d1149a4 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/BeanUtils.java @@ -30,13 +30,14 @@ public abstract class BeanUtils { public static ConfigurableConversionService CONVERSION_SERVICE = new DefaultConversionService(); - private static final LoadingCache fields = CacheBuilder.newBuilder().build( + private static final LoadingCache fields = CacheBuilder.newBuilder().build( new CacheLoader() { - @Override public Field load(Object[] key) throws Exception { - Class clazz = (Class) key[0]; - String name = (String) key[1]; + @Override public Field load(Object[] key) + throws Exception { + Class clazz = (Class)key[0]; + String name = (String)key[1]; Field f = ReflectionUtils.findField(clazz, name); - if (null != f) { + if(null != f) { ReflectionUtils.makeAccessible(f); return f; } else { @@ -47,14 +48,15 @@ public abstract class BeanUtils { ); private static final LoadingCache methods = CacheBuilder.newBuilder().build( new CacheLoader() { - @Override public Method load(Object[] key) throws Exception { - Class clazz = (Class) key[0]; - String name = (String) key[1]; - Integer paramCnt = key.length == 3 ? (Integer) key[2] : 0; + @Override public Method load(Object[] key) + throws Exception { + Class clazz = (Class)key[0]; + String name = (String)key[1]; + Integer paramCnt = key.length == 3 ? (Integer)key[2] : 0; - for (Method m : clazz.getDeclaredMethods()) { - if (m.getName().equals(name)) { - if (m.getParameterTypes().length == paramCnt) { + for(Method m : clazz.getDeclaredMethods()) { + if(m.getName().equals(name)) { + if(m.getParameterTypes().length == paramCnt) { ReflectionUtils.makeAccessible(m); return m; } @@ -67,28 +69,28 @@ public abstract class BeanUtils { ); public static boolean hasProperty(String property, Object... objs) { - for (Object obj : objs) { - if (obj instanceof Map) { - return ((Map) obj).containsKey(property); + for(Object obj : objs) { + if(obj instanceof Map) { + return ((Map)obj).containsKey(property); } Class type = obj.getClass(); try { - if (FluentBeanUtils.isFluentBean(type)) { + if(FluentBeanUtils.isFluentBean(type)) { return null != methods.get(new Object[]{type, property}); } else { - if (null == methods.get(new Object[]{type, "get" + StringUtils.capitalize(property)})) { + if(null == methods.get(new Object[]{type, "get" + StringUtils.capitalize(property)})) { return null != fields.get(new Object[]{type, property}); } else { return true; } } - } catch (UncheckedExecutionException e) { - if (e.getCause().getClass() == IllegalArgumentException.class) { + } catch(UncheckedExecutionException e) { + if(e.getCause().getClass() == IllegalArgumentException.class) { return false; } else { throw new IllegalStateException(e); } - } catch (ExecutionException e) { + } catch(ExecutionException e) { throw new IllegalStateException(e); } } @@ -97,9 +99,9 @@ public abstract class BeanUtils { @SuppressWarnings({"unchecked"}) public static T findFirst(Class clazz, List stack) { - for (Object o : stack) { - if (ClassUtils.isAssignable(clazz, o.getClass())) { - return (T) o; + for(Object o : stack) { + if(ClassUtils.isAssignable(clazz, o.getClass())) { + return (T)o; } } return null; @@ -107,44 +109,44 @@ public abstract class BeanUtils { @SuppressWarnings({"unchecked"}) public static Object findFirst(Object o, Object... objs) { - for (Object obj : objs) { - if (o == obj || null != o && o.equals(obj)) { + for(Object obj : objs) { + if(o == obj || null != o && o.equals(obj)) { return obj; - } else if (obj instanceof List) { - return Collections.binarySearch((List) obj, o); - } else if (obj instanceof Object[]) { - return Arrays.binarySearch((Object[]) obj, o); + } else if(obj instanceof List) { + return Collections.binarySearch((List)obj, o); + } else if(obj instanceof Object[]) { + return Arrays.binarySearch((Object[])obj, o); } } return null; } public static Object findFirst(String property, Object... objs) { - for (Object obj : objs) { - if (obj instanceof Map) { - return ((Map) obj).get(property); + for(Object obj : objs) { + if(obj instanceof Map) { + return ((Map)obj).get(property); } Class type = obj.getClass(); try { Field f = fields.get(new Object[]{type, property}); - if (FluentBeanUtils.isFluentBean(type)) { + if(FluentBeanUtils.isFluentBean(type)) { return FluentBeanUtils.get(property, obj); } else { Method getter = methods.get(new Object[]{type, "get" + StringUtils.capitalize(property)}); try { - if (null != getter) { + if(null != getter) { return getter.invoke(obj); } else { return f.get(obj); } - } catch (IllegalAccessException e) { + } catch(IllegalAccessException e) { throw new IllegalStateException(e); - } catch (InvocationTargetException e) { + } catch(InvocationTargetException e) { throw new IllegalStateException(e); } } - } catch (IllegalArgumentException e) { - } catch (ExecutionException e) { + } catch(IllegalArgumentException e) { + } catch(ExecutionException e) { throw new IllegalArgumentException(e); } } @@ -157,8 +159,8 @@ public abstract class BeanUtils { } public static boolean containsType(Class type, Object[] objs) { - for (Object obj : objs) { - if (null != obj && ClassUtils.isAssignable(obj.getClass(), type)) { + for(Object obj : objs) { + if(null != obj && ClassUtils.isAssignable(obj.getClass(), type)) { return true; } } @@ -172,7 +174,7 @@ public abstract class BeanUtils { @SuppressWarnings({"unchecked"}) public static T invoke(String methodName, Object target, Class returnType, Object... args) { - if (null == target) { + if(null == target) { return null; } @@ -181,11 +183,11 @@ public abstract class BeanUtils { Method m = methods.get(new Object[]{type, methodName, args.length}); List newArgs = new ArrayList(args.length); Class[] paramTypes = m.getParameterTypes(); - for (int i = 0; i < args.length; i++) { + for(int i = 0; i < args.length; i++) { Object o = args[i]; Class oType = o.getClass(); Class pType = paramTypes[i]; - if (!ClassUtils.isAssignable(oType, pType)) { + if(!ClassUtils.isAssignable(oType, pType)) { newArgs.add(CONVERSION_SERVICE.convert(o, pType)); } else { newArgs.add(o); @@ -193,15 +195,15 @@ public abstract class BeanUtils { } Object rtnVal = m.invoke(target, newArgs.toArray()); - if ((returnType != Void.TYPE || returnType != Object.class) + if((returnType != Void.TYPE || returnType != Object.class) && null != rtnVal && !ClassUtils.isAssignable(returnType, rtnVal.getClass())) { return CONVERSION_SERVICE.convert(rtnVal, returnType); } else { - return (T) rtnVal; + return (T)rtnVal; } - } catch (IllegalArgumentException e) { - } catch (Exception e) { + } catch(IllegalArgumentException e) { + } catch(Exception e) { throw new IllegalStateException(e); } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java index 2b2e7b4f4..c0a342e48 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanDeserializer.java @@ -18,9 +18,10 @@ import org.springframework.util.ClassUtils; * * @author Jon Brisbin */ -public class FluentBeanDeserializer extends StdDeserializer { +public class FluentBeanDeserializer + extends StdDeserializer { - private ConversionService conversionService; + private ConversionService conversionService; private FluentBeanUtils.Metadata beanMeta; @SuppressWarnings({"unchecked"}) @@ -29,7 +30,7 @@ public class FluentBeanDeserializer extends StdDeserializer { this.conversionService = conversionService; this.beanMeta = FluentBeanUtils.metadata(valueClass); - if (!FluentBeanUtils.isFluentBean(valueClass)) { + if(!FluentBeanUtils.isFluentBean(valueClass)) { throw new IllegalArgumentException("Class of type " + valueClass + " is not a FluentBean"); } } @@ -39,46 +40,46 @@ public class FluentBeanDeserializer extends StdDeserializer { DeserializationContext ctxt) throws IOException, JsonProcessingException { - if (jp.getCurrentToken() != JsonToken.START_OBJECT) { + if(jp.getCurrentToken() != JsonToken.START_OBJECT) { throw ctxt.mappingException(_valueClass); } Object bean; try { bean = _valueClass.newInstance(); - } catch (InstantiationException e) { + } catch(InstantiationException e) { throw new IllegalStateException(e); - } catch (IllegalAccessException e) { + } catch(IllegalAccessException e) { throw new IllegalStateException(e); } - while (jp.nextToken() != JsonToken.END_OBJECT) { + while(jp.nextToken() != JsonToken.END_OBJECT) { String name = jp.getCurrentName(); Method setter = beanMeta.setters().get(name); Object obj; - if (null != setter) { + if(null != setter) { Class targetType = setter.getParameterTypes()[0]; - if (ClassUtils.isAssignable(targetType, Long.class)) { + if(ClassUtils.isAssignable(targetType, Long.class)) { obj = jp.nextLongValue(-1); - } else if (ClassUtils.isAssignable(targetType, Integer.class)) { + } else if(ClassUtils.isAssignable(targetType, Integer.class)) { obj = jp.nextIntValue(-1); - } else if (ClassUtils.isAssignable(targetType, Boolean.class)) { + } else if(ClassUtils.isAssignable(targetType, Boolean.class)) { obj = jp.nextBooleanValue(); } else { obj = jp.nextTextValue(); } - if (null != obj) { - if (!ClassUtils.isAssignable(obj.getClass(), targetType)) { + if(null != obj) { + if(!ClassUtils.isAssignable(obj.getClass(), targetType)) { obj = conversionService.convert(obj, targetType); } try { setter.invoke(bean, obj); - } catch (IllegalAccessException e) { + } catch(IllegalAccessException e) { throw new IllegalStateException(e); - } catch (InvocationTargetException e) { + } catch(InvocationTargetException e) { throw new IllegalStateException(e); } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java index 7f1bfec34..463aba1dd 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanSerializer.java @@ -16,60 +16,62 @@ import org.springframework.util.ClassUtils; * * @author Jon Brisbin */ -public class FluentBeanSerializer extends SerializerBase { +public class FluentBeanSerializer + extends SerializerBase { @SuppressWarnings({"unchecked", "rawtypes"}) - public FluentBeanSerializer( final Class t ) { - super( t ); + public FluentBeanSerializer(final Class t) { + super(t); - if ( !FluentBeanUtils.isFluentBean( t ) ) { - throw new IllegalArgumentException( "Class of type " + t + " is not a FluentBean" ); + if(!FluentBeanUtils.isFluentBean(t)) { + throw new IllegalArgumentException("Class of type " + t + " is not a FluentBean"); } } @SuppressWarnings({"unchecked"}) @Override - public void serialize( final Object value, - final JsonGenerator jgen, - final SerializerProvider provider ) + public void serialize(final Object value, + final JsonGenerator jgen, + final SerializerProvider provider) throws IOException, JsonGenerationException { - if ( null == value ) { - provider.defaultSerializeNull( jgen ); + if(null == value) { + provider.defaultSerializeNull(jgen); } else { Class type = value.getClass(); - if ( ClassUtils.isAssignable( type, Collection.class ) ) { + if(ClassUtils.isAssignable(type, Collection.class)) { jgen.writeStartArray(); - for ( Object o : (Collection) value ) { - write( o, jgen, provider ); + for(Object o : (Collection)value) { + write(o, jgen, provider); } jgen.writeEndArray(); - } else if ( ClassUtils.isAssignable( type, Map.class ) ) { + } else if(ClassUtils.isAssignable(type, Map.class)) { jgen.writeStartObject(); - for ( Map.Entry entry : ((Map) value).entrySet() ) { - jgen.writeFieldName( entry.getKey() ); - write( entry.getValue(), jgen, provider ); + for(Map.Entry entry : ((Map)value).entrySet()) { + jgen.writeFieldName(entry.getKey()); + write(entry.getValue(), jgen, provider); } jgen.writeEndObject(); } else { - write( value, jgen, provider ); + write(value, jgen, provider); } } } - private void write( final Object value, - final JsonGenerator jgen, - final SerializerProvider provider ) throws IOException { + private void write(final Object value, + final JsonGenerator jgen, + final SerializerProvider provider) + throws IOException { Class type = value.getClass(); - if ( ClassUtils.isAssignable( type, _handledType ) ) { + if(ClassUtils.isAssignable(type, _handledType)) { jgen.writeStartObject(); - for ( String fname : FluentBeanUtils.metadata( type ).fieldNames() ) { - jgen.writeFieldName( fname ); - write( FluentBeanUtils.get( fname, value ), jgen, provider ); + for(String fname : FluentBeanUtils.metadata(type).fieldNames()) { + jgen.writeFieldName(fname); + write(FluentBeanUtils.get(fname, value), jgen, provider); } jgen.writeEndObject(); } else { - jgen.writeObject( value ); + jgen.writeObject(value); } } diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java index 791c414f1..e820d45c9 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/FluentBeanUtils.java @@ -22,24 +22,27 @@ import org.springframework.util.ReflectionUtils; */ public abstract class FluentBeanUtils { - private static final Logger log = LoggerFactory.getLogger(FluentBeanUtils.class); + private static final Logger log = LoggerFactory.getLogger(FluentBeanUtils.class); private static final LoadingCache, Metadata> metadata = CacheBuilder.newBuilder().build( new CacheLoader, Metadata>() { - @Override public Metadata load(Class type) throws Exception { + @Override public Metadata load(Class type) + throws Exception { final Metadata meta = new Metadata(); ReflectionUtils.doWithFields( type, new ReflectionUtils.FieldCallback() { - @Override public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { + @Override public void doWith(Field field) + throws IllegalArgumentException, IllegalAccessException { final String fname = field.getName(); - if (!fname.startsWith("_")) { + if(!fname.startsWith("_")) { ReflectionUtils.doWithMethods(field.getDeclaringClass(), new ReflectionUtils.MethodCallback() { @Override - public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - if (method.getName().equals(fname)) { - if (method.getParameterTypes().length == 0) { + public void doWith(Method method) + throws IllegalArgumentException, IllegalAccessException { + if(method.getName().equals(fname)) { + if(method.getParameterTypes().length == 0) { meta.getters.put(fname, method); - } else if (method.getParameterTypes().length == 1) { + } else if(method.getParameterTypes().length == 1) { meta.setters.put(fname, method); } meta.fieldNames.add(fname); @@ -58,13 +61,15 @@ public abstract class FluentBeanUtils { /** * Interrogate a bean and collect {@link Metadata} on it. * - * @param targetType The type to interrogate. + * @param targetType + * The type to interrogate. + * * @return {@link Metadata} for the fluent bean. */ public static Metadata metadata(Class targetType) { try { return metadata.get(targetType); - } catch (ExecutionException e) { + } catch(ExecutionException e) { throw new IllegalStateException(e); } } @@ -72,27 +77,31 @@ public abstract class FluentBeanUtils { /** * Set the property of a fluent bean. * - * @param property Name of the property to set. - * @param value Value of the property. - * @param bean Bean on which to set this property. + * @param property + * Name of the property to set. + * @param value + * Value of the property. + * @param bean + * Bean on which to set this property. + * * @return Usually {@literal null} but will return whatever the "setter" returns, which could be {@this} or something * else. */ public static Object set(String property, Object value, Object bean) { - if (null == bean) { + if(null == bean) { return null; } Class type = bean.getClass(); try { Method setter = metadata.get(type).setters.get(property); - if (null != setter) { + if(null != setter) { return setter.invoke(bean, value); } else { return null; } - } catch (Throwable t) { - if (log.isDebugEnabled()) { + } catch(Throwable t) { + if(log.isDebugEnabled()) { log.debug(t.getMessage(), t); } return null; @@ -102,25 +111,28 @@ public abstract class FluentBeanUtils { /** * Get the value of a property. * - * @param property Name of the property. - * @param bean Bean of which to get the property. + * @param property + * Name of the property. + * @param bean + * Bean of which to get the property. + * * @return Value of the property. Could be {@literal null} */ public static Object get(String property, Object bean) { - if (null == bean) { + if(null == bean) { return null; } Class type = bean.getClass(); try { Method getter = metadata.get(type).getters.get(property); - if (null != getter) { + if(null != getter) { return getter.invoke(bean); } else { return null; } - } catch (Throwable t) { - if (log.isDebugEnabled()) { + } catch(Throwable t) { + if(log.isDebugEnabled()) { log.debug(t.getMessage(), t); } return null; @@ -132,21 +144,23 @@ public abstract class FluentBeanUtils { * to a field of the same name. A "getter" is that method which is named the same as the field and has 0 parameters. * The "setter" is that method which is named the same as the field and has a single argument. * - * @param type The class to inspect. + * @param type + * The class to inspect. + * * @return {@literal true} if this looks like a fluent bean, {@literal false} otherwise. */ public static boolean isFluentBean(Class type) { try { return metadata.get(type).getters.size() > 0; - } catch (ExecutionException e) { + } catch(ExecutionException e) { throw new IllegalStateException(e); } } public static class Metadata { - List fieldNames = new ArrayList(); - Map getters = new HashMap(); - Map setters = new HashMap(); + List fieldNames = new ArrayList(); + Map getters = new HashMap(); + Map setters = new HashMap(); public List fieldNames() { return fieldNames; diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java index b4503f9eb..d7ec4d2ad 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/util/UriUtils.java @@ -24,8 +24,11 @@ public abstract class UriUtils { * http://localhost:8080/data/person}, this method would report the baseUri being a valid base of the given URI. *

* - * @param baseUri {@link URI} to check. - * @param uri {@link URI} against which to compare the base. + * @param baseUri + * {@link URI} to check. + * @param uri + * {@link URI} against which to compare the base. + * * @return {@literal true} if the baseUri is valid against the given {@link URI}, {@literal false} otherwise. */ public static boolean validBaseUri(URI baseUri, URI uri) { @@ -41,16 +44,21 @@ public abstract class UriUtils { * second time passing a relative {@link URI} of "1". *

* - * @param baseUri base {@link URI} - * @param uri {@link URI} to explode and iteratre over. - * @param handler {@link Handler} to call for each segment of the URI's path. - * @param Return type of the handler. + * @param baseUri + * base {@link URI} + * @param uri + * {@link URI} to explode and iteratre over. + * @param handler + * {@link Handler} to call for each segment of the URI's path. + * @param + * Return type of the handler. + * * @return Handler return value, or possibly {@literal null}. */ public static V foreach(URI baseUri, URI uri, Handler handler) { List uris = explode(baseUri, uri); V v = null; - for (URI u : uris) { + for(URI u : uris) { v = handler.handle(u); } return v; @@ -63,16 +71,19 @@ public abstract class UriUtils { * in * a {@link Stack} of relative {@link URI}s of size 2--one for "person" and one for "1".

* - * @param baseUri base {@link URI} - * @param uri {@link URI} to explode + * @param baseUri + * base {@link URI} + * @param uri + * {@link URI} to explode + * * @return {@link Stack} of relative {@link URI}s. */ public static Stack explode(URI baseUri, URI uri) { Stack uris = new Stack(); - if (StringUtils.hasText(uri.getPath())) { + if(StringUtils.hasText(uri.getPath())) { URI relativeUri = baseUri.relativize(uri); - if (StringUtils.hasText(relativeUri.getPath())) { - for (String part : relativeUri.getPath().split("/")) { + if(StringUtils.hasText(relativeUri.getPath())) { + for(String part : relativeUri.getPath().split("/")) { uris.add(URI.create(part + (StringUtils.hasText(uri.getQuery()) ? "?" + uri.getQuery() : ""))); } } @@ -86,38 +97,41 @@ public abstract class UriUtils { *

e.g. merging base URI {@literal http://localhost:8080/data} and relative uri {@literal person/1?name=John+Doe} * would result in an absolute URI of {@literal http://localhost:8080/data/person/1?name=John+Doe}

* - * @param baseUri base {@link URI} - * @param uris {@link URI}s to merge + * @param baseUri + * base {@link URI} + * @param uris + * {@link URI}s to merge + * * @return {@link URI} that is the combination of all the given (possibly relative, possibly absolute) URIs. */ public static URI merge(URI baseUri, URI... uris) { StringBuilder query = new StringBuilder(); UriComponentsBuilder ub = UriComponentsBuilder.fromUri(baseUri); - for (URI uri : uris) { + for(URI uri : uris) { String s = uri.getScheme(); - if (null != s) { + if(null != s) { ub.scheme(s); } s = uri.getUserInfo(); - if (null != s) { + if(null != s) { ub.userInfo(s); } s = uri.getHost(); - if (null != s) { + if(null != s) { ub.host(s); } int i = uri.getPort(); - if (i > 0) { + if(i > 0) { ub.port(i); } s = uri.getPath(); - if (null != s) { - if (!uri.isAbsolute() && StringUtils.hasText(s)) { + if(null != s) { + if(!uri.isAbsolute() && StringUtils.hasText(s)) { ub.pathSegment(s); } else { ub.path(s); @@ -125,20 +139,20 @@ public abstract class UriUtils { } s = uri.getQuery(); - if (null != s) { - if (query.length() > 0) { + if(null != s) { + if(query.length() > 0) { query.append("&"); } query.append(s); } s = uri.getFragment(); - if (null != s) { + if(null != s) { ub.fragment(s); } } - if (query.length() > 0) { + if(query.length() > 0) { ub.query(query.toString()); } @@ -149,14 +163,15 @@ public abstract class UriUtils { * Just the path portion of the {@link URI}, but with any trailing slash "/" removed. * * @param uri + * * @return */ public static String path(URI uri) { - if (null == uri) { + if(null == uri) { return null; } String s = uri.getPath(); - if (s.endsWith("/")) { + if(s.endsWith("/")) { return s.substring(0, s.length() - 1); } else { return s; @@ -166,8 +181,11 @@ public abstract class UriUtils { /** * The very last segment of the {@link URI}. * - * @param baseUri base {@link URI} - * @param uri {@link URI} to explode + * @param baseUri + * base {@link URI} + * @param uri + * {@link URI} to explode + * * @return Relative {@link URI} that is the last segment of the path for the given URI. */ public static URI tail(URI baseUri, URI uri) { diff --git a/spring-data-rest-repository/build.gradle b/spring-data-rest-repository/build.gradle index 482ac1852..40eec5a77 100644 --- a/spring-data-rest-repository/build.gradle +++ b/spring-data-rest-repository/build.gradle @@ -1,15 +1,14 @@ dependencies { // Spring - compile("org.springframework:spring-orm:$springVersion") { force = true } - compile("org.springframework:spring-oxm:$springVersion") { force = true } - compile("org.springframework:spring-tx:$springVersion") { force = true } + //compile("org.springframework:spring-orm:$springVersion") { force = true } + //compile("org.springframework:spring-oxm:$springVersion") { force = true } // JPA compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final" // Spring Data - compile "org.springframework.data:spring-data-commons-core:$sdCommonsVersion" + //compile "org.springframework.data:spring-data-commons-core:$sdCommonsVersion" compile "org.springframework.data:spring-data-jpa:$sdJpaVersion" // Exporter core diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java index 904e870c7..584fdb03f 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/AttributeMetadata.java @@ -42,7 +42,9 @@ public interface AttributeMetadata { /** * Get the path of this attribute as a {@link Collection}. * - * @param target The entity to inspect for this attribute. + * @param target + * The entity to inspect for this attribute. + * * @return attribute value as a {@link Collection} */ Collection asCollection(Object target); @@ -57,7 +59,9 @@ public interface AttributeMetadata { /** * Get the path of this attribute as a {@link Set}. * - * @param target The entity to inspect for this attribute. + * @param target + * The entity to inspect for this attribute. + * * @return attribute value as a {@link Set} */ Set asSet(Object target); @@ -72,7 +76,9 @@ public interface AttributeMetadata { /** * Get the path of this attribute as a {@link Map}. * - * @param target The entity to inspect for this attribute. + * @param target + * The entity to inspect for this attribute. + * * @return attribute value as a {@link Map} */ Map asMap(Object target); @@ -80,7 +86,9 @@ public interface AttributeMetadata { /** * Get the path of this attribute. * - * @param target The entity to inspect for this attribute. + * @param target + * The entity to inspect for this attribute. + * * @return attribute value */ Object get(Object target); @@ -88,8 +96,11 @@ public interface AttributeMetadata { /** * Set the path of this attribute. * - * @param value Value to set on this attribute. - * @param target The entity to set this attribute's value on. + * @param value + * Value to set on this attribute. + * @param target + * The entity to set this attribute's value on. + * * @return @this */ AttributeMetadata set(Object value, Object target); diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/EntityMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/EntityMetadata.java index bbf9660bf..9bd40d63b 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/EntityMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/EntityMetadata.java @@ -47,7 +47,9 @@ public interface EntityMetadata { /** * Get {@link AttributeMetadata} by name. * - * @param name The name of the attribute. + * @param name + * The name of the attribute. + * * @return {@link AttributeMetadata} or {@literal null} if that attribute doesn't exist. */ A attribute(String name); diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java index 44752c4ab..bcdcf811a 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryConstraintViolationException.java @@ -6,7 +6,8 @@ import org.springframework.validation.Errors; /** * @author Jon Brisbin */ -public class RepositoryConstraintViolationException extends DataIntegrityViolationException { +public class RepositoryConstraintViolationException + extends DataIntegrityViolationException { private Errors errors; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporter.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporter.java index 90266b13f..92ae7d6c5 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporter.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporter.java @@ -25,7 +25,7 @@ public abstract class RepositoryExporter, E exte InitializingBean { protected ApplicationContext applicationContext; - protected Repositories repositories; + protected Repositories repositories; protected List exportOnlyTheseClasses = Collections.emptyList(); protected Map repositoryMetadata; @@ -42,22 +42,24 @@ public abstract class RepositoryExporter, E exte * Set the class names of only those Repositories you want exported. * Default is to export all found Repositories. * - * @param exportOnlyTheseClasses {@link List} of class names to export. + * @param exportOnlyTheseClasses + * {@link List} of class names to export. + * * @return @this */ @SuppressWarnings({"unchecked"}) public M setExportOnlyTheseClasses(List exportOnlyTheseClasses) { this.exportOnlyTheseClasses = exportOnlyTheseClasses; - return (M) this; + return (M)this; } - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + @Override public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { this.applicationContext = applicationContext; } @SuppressWarnings({"unchecked"}) @Override public void afterPropertiesSet() throws Exception { - } /** @@ -66,20 +68,22 @@ public abstract class RepositoryExporter, E exte * @return {@link List} of class names to export. */ public Set repositoryNames() { - findRepositories(); + refresh(); return repositoryMetadata.keySet(); } /** * Is a Repository being exporter that supports this domain type? * - * @param domainType Type of the domain class. + * @param domainType + * Type of the domain class. + * * @return {@literal true} if a Repository is being exported, {@literal false} otherwise. */ public boolean hasRepositoryFor(Class domainType) { - findRepositories(); - for (M repoMeta : repositoryMetadata.values()) { - if (repoMeta.domainType().isAssignableFrom(domainType)) { + refresh(); + for(M repoMeta : repositoryMetadata.values()) { + if(repoMeta.domainType().isAssignableFrom(domainType)) { return true; } } @@ -89,13 +93,15 @@ public abstract class RepositoryExporter, E exte /** * Get the RepositoryMetadata for the Repository responsible for this domain type. * - * @param domainType Type of the domain class. + * @param domainType + * Type of the domain class. + * * @return {@link RepositoryMetadata} instance */ public M repositoryMetadataFor(Class domainType) { - findRepositories(); - for (M repoMeta : repositoryMetadata.values()) { - if (repoMeta.domainType().isAssignableFrom(domainType)) { + refresh(); + for(M repoMeta : repositoryMetadata.values()) { + if(repoMeta.domainType().isAssignableFrom(domainType)) { return repoMeta; } } @@ -105,11 +111,13 @@ public abstract class RepositoryExporter, E exte /** * Get the {@link RepositoryMetadata} for the Repository exported under the given name. * - * @param name Name a Repository would be exported under. + * @param name + * Name a Repository would be exported under. + * * @return {@link RepositoryMetadata} instance */ public M repositoryMetadataFor(String name) { - findRepositories(); + refresh(); return repositoryMetadata.get(name); } @@ -118,25 +126,27 @@ public abstract class RepositoryExporter, E exte Class repoClass, Repositories repositories); - private void findRepositories() { - if (null == repositories) { - repositories = new Repositories(applicationContext); - repositoryMetadata = new HashMap(); - for (Class domainType : repositories) { - if (exportOnlyTheseClasses.isEmpty() || exportOnlyTheseClasses.contains(domainType.getName())) { - Class repoClass = repositories.getRepositoryInformationFor(domainType).getRepositoryInterface(); - String name = StringUtils.uncapitalize(repoClass.getSimpleName().replaceAll("Repository", "")); - RestResource resourceAnno = repoClass.getAnnotation(RestResource.class); - boolean exported = true; - if (null != resourceAnno) { - if (StringUtils.hasText(resourceAnno.path())) { - name = resourceAnno.path(); - } - exported = resourceAnno.exported(); - } - if (exported) { - repositoryMetadata.put(name, createRepositoryMetadata(name, domainType, repoClass, repositories)); + @SuppressWarnings({"unchecked"}) + public void refresh() { + if(null != repositories) { + return; + } + repositories = new Repositories(applicationContext); + repositoryMetadata = new HashMap(); + for(Class domainType : repositories) { + if(exportOnlyTheseClasses.isEmpty() || exportOnlyTheseClasses.contains(domainType.getName())) { + Class repoClass = repositories.getRepositoryInformationFor(domainType).getRepositoryInterface(); + String name = StringUtils.uncapitalize(repoClass.getSimpleName().replaceAll("Repository", "")); + RestResource resourceAnno = repoClass.getAnnotation(RestResource.class); + boolean exported = true; + if(null != resourceAnno) { + if(StringUtils.hasText(resourceAnno.path())) { + name = resourceAnno.path(); } + exported = resourceAnno.exported(); + } + if(exported) { + repositoryMetadata.put(name, createRepositoryMetadata(name, domainType, repoClass, repositories)); } } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java index e1710362d..f5e8c8b0f 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryExporterSupport.java @@ -28,7 +28,8 @@ public abstract class RepositoryExporterSupport repositoryExporters) { this.repositoryExporters = repositoryExporters; @@ -46,39 +47,44 @@ public abstract class RepositoryExporterSupport repositoryExporters) { setRepositoryExporters(repositoryExporters); - return (S) this; + return (S)this; } /** * Set the {@link RepositoryExporter}s to use. * * @param repositoryExporter + * * @return */ @SuppressWarnings({"unchecked"}) public S repositoryExporters(RepositoryExporter... repositoryExporter) { setRepositoryExporters(Arrays.asList(repositoryExporter)); - return (S) this; + return (S)this; } /** * Find {@link RepositoryMetadata} for the {@link org.springframework.data.repository.Repository} exported under this * name. * - * @param name URL segment name. + * @param name + * URL segment name. + * * @return {@link RepositoryMetadata} or {@literal null} if none found. */ @SuppressWarnings({"unchecked"}) protected RepositoryMetadata repositoryMetadataFor(String name) { - for (RepositoryExporter exporter : repositoryExporters) { + for(RepositoryExporter exporter : repositoryExporters) { RepositoryMetadata repoMeta = exporter.repositoryMetadataFor(name); - if (null != repoMeta) { + if(null != repoMeta) { return repoMeta; } } @@ -89,14 +95,16 @@ public abstract class RepositoryExporterSupport domainType) { - for (RepositoryExporter exporter : repositoryExporters) { + for(RepositoryExporter exporter : repositoryExporters) { RepositoryMetadata repoMeta = exporter.repositoryMetadataFor(domainType); - if (null != repoMeta) { + if(null != repoMeta) { return repoMeta; } } @@ -107,12 +115,14 @@ public abstract class RepositoryExporterSupport */ -public class RepositoryNotFoundException extends DataAccessResourceFailureException { +public class RepositoryNotFoundException + extends DataAccessResourceFailureException { public RepositoryNotFoundException(String msg) { super(msg); diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryQueryMethod.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryQueryMethod.java deleted file mode 100644 index ec0f2343a..000000000 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryQueryMethod.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.springframework.data.rest.repository; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; - -import org.springframework.core.LocalVariableTableParameterNameDiscoverer; -import org.springframework.data.repository.query.Param; - -/** - * @author Jon Brisbin - */ -public class RepositoryQueryMethod { - - private static final LocalVariableTableParameterNameDiscoverer nameLookup = new LocalVariableTableParameterNameDiscoverer(); - - private Method method; - private Class[] paramTypes; - private String[] paramNames; - - public RepositoryQueryMethod(Method method) { - this.method = method; - paramTypes = method.getParameterTypes(); - paramNames = nameLookup.getParameterNames(method); - if (null == paramNames) { - paramNames = new String[paramTypes.length]; - } - Annotation[][] paramAnnos = method.getParameterAnnotations(); - for (int i = 0; i < paramAnnos.length; i++) { - if (paramAnnos[i].length > 0) { - for (Annotation anno : paramAnnos[i]) { - if (Param.class.isAssignableFrom(anno.getClass())) { - Param p = (Param) anno; - paramNames[i] = p.value(); - break; - } - } - } - if (null == paramNames[i]) { - paramNames[i] = "arg" + i; - } - } - } - - public Class[] paramTypes() { - return paramTypes; - } - - public String[] paramNames() { - return paramNames; - } - - public Method method() { - return method; - } - -} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java index ed6982344..eb646c150 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/ValidationErrors.java @@ -11,13 +11,14 @@ import org.springframework.validation.ObjectError; /** * @author Jon Brisbin */ -public class ValidationErrors extends AbstractErrors { +public class ValidationErrors + extends AbstractErrors { - private String name; - private Object entity; + private String name; + private Object entity; private EntityMetadata entityMetadata; private List globalErrors = new ArrayList(); - private List fieldErrors = new ArrayList(); + private List fieldErrors = new ArrayList(); public ValidationErrors(String name, Object entity, EntityMetadata entityMetadata) { this.name = name; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java index e0b7f78ab..b56980aac 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AbstractRepositoryEventListener.java @@ -23,7 +23,8 @@ public abstract class AbstractRepositoryEventListener */ -public class AfterDeleteEvent extends RepositoryEvent { +public class AfterDeleteEvent + extends RepositoryEvent { public AfterDeleteEvent(Object source) { super(source); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterLinkSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterLinkSaveEvent.java index b316f9841..825317042 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterLinkSaveEvent.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterLinkSaveEvent.java @@ -5,7 +5,8 @@ package org.springframework.data.rest.repository.context; * * @author Jon Brisbin */ -public class AfterLinkSaveEvent extends LinkSaveEvent { +public class AfterLinkSaveEvent + extends LinkSaveEvent { public AfterLinkSaveEvent(Object source, Object child) { super(source, child); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterSaveEvent.java index 3df0d9eca..895f7405e 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterSaveEvent.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AfterSaveEvent.java @@ -5,7 +5,8 @@ package org.springframework.data.rest.repository.context; * * @author Jon Brisbin */ -public class AfterSaveEvent extends RepositoryEvent { +public class AfterSaveEvent + extends RepositoryEvent { public AfterSaveEvent(Object source) { super(source); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AnnotatedHandlerRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AnnotatedHandlerRepositoryEventListener.java index 3200dda6c..2ba6524a9 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AnnotatedHandlerRepositoryEventListener.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/AnnotatedHandlerRepositoryEventListener.java @@ -37,7 +37,7 @@ public class AnnotatedHandlerRepositoryEventListener ApplicationContextAware, InitializingBean { - private String basePackage; + private String basePackage; private ApplicationContext applicationContext; private Multimap, EventHandlerMethod> handlerMethods = ArrayListMultimap.create(); @@ -48,7 +48,8 @@ public class AnnotatedHandlerRepositoryEventListener this.basePackage = basePackage; } - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + @Override public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { this.applicationContext = applicationContext; } @@ -64,7 +65,9 @@ public class AnnotatedHandlerRepositoryEventListener /** * Set the base package in which to search for event handlers. * - * @param basePackage Base package to search for handlers. + * @param basePackage + * Base package to search for handlers. + * * @return @this */ public AnnotatedHandlerRepositoryEventListener setBasePackage(String basePackage) { @@ -84,7 +87,9 @@ public class AnnotatedHandlerRepositoryEventListener /** * Set the base package in which to search for event handlers. * - * @param basePackage Base package to search for handlers. + * @param basePackage + * Base package to search for handlers. + * * @return @this */ public AnnotatedHandlerRepositoryEventListener basePackage(String basePackage) { @@ -92,23 +97,25 @@ public class AnnotatedHandlerRepositoryEventListener return this; } - @Override public void afterPropertiesSet() throws Exception { + @Override public void afterPropertiesSet() + throws Exception { ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); scanner.addIncludeFilter(new AnnotationTypeFilter(RepositoryEventHandler.class, true, true)); - for (BeanDefinition beanDef : scanner.findCandidateComponents(basePackage)) { + for(BeanDefinition beanDef : scanner.findCandidateComponents(basePackage)) { String typeName = beanDef.getBeanClassName(); Class handlerType = ClassUtils.forName(typeName, ClassUtils.getDefaultClassLoader()); RepositoryEventHandler typeAnno = handlerType.getAnnotation(RepositoryEventHandler.class); Class[] targetTypes = typeAnno.value(); - if (targetTypes.length == 0) { + if(targetTypes.length == 0) { targetTypes = new Class[]{null}; } - for (final Class targetType : targetTypes) { - for (final Object handler : applicationContext.getBeansOfType(handlerType).values()) { + for(final Class targetType : targetTypes) { + for(final Object handler : applicationContext.getBeansOfType(handlerType).values()) { ReflectionUtils.doWithMethods( handler.getClass(), new ReflectionUtils.MethodCallback() { - @Override public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + @Override public void doWith(Method method) + throws IllegalArgumentException, IllegalAccessException { inspect(targetType, handler, method, HandleBeforeSave.class, BeforeSaveEvent.class); inspect(targetType, handler, method, HandleAfterSave.class, AfterSaveEvent.class); inspect(targetType, handler, method, HandleBeforeLinkSave.class, BeforeLinkSaveEvent.class); @@ -133,21 +140,21 @@ public class AnnotatedHandlerRepositoryEventListener @Override public void onApplicationEvent(RepositoryEvent event) { Class eventType = event.getClass(); - if (handlerMethods.containsKey(eventType)) { - for (EventHandlerMethod handlerMethod : handlerMethods.get(eventType)) { + if(handlerMethods.containsKey(eventType)) { + for(EventHandlerMethod handlerMethod : handlerMethods.get(eventType)) { try { Object src = event.getSource(); - if (ClassUtils.isAssignable(handlerMethod.targetType, src.getClass())) { + if(ClassUtils.isAssignable(handlerMethod.targetType, src.getClass())) { List params = new ArrayList(); params.add(src); - if (event instanceof BeforeLinkSaveEvent) { - params.add(((BeforeLinkSaveEvent) event).getLinked()); - } else if (event instanceof AfterLinkSaveEvent) { - params.add(((AfterLinkSaveEvent) event).getLinked()); + if(event instanceof BeforeLinkSaveEvent) { + params.add(((BeforeLinkSaveEvent)event).getLinked()); + } else if(event instanceof AfterLinkSaveEvent) { + params.add(((AfterLinkSaveEvent)event).getLinked()); } handlerMethod.method.invoke(handlerMethod.handler, params.toArray()); } - } catch (Exception e) { + } catch(Exception e) { throw new IllegalStateException(e); } } @@ -160,31 +167,31 @@ public class AnnotatedHandlerRepositoryEventListener Class annoType, Class eventType) { T anno = method.getAnnotation(annoType); - if (null != anno) { + if(null != anno) { try { Class[] targetTypes; - if (null == targetType) { - targetTypes = (Class[]) anno.getClass().getMethod("value", new Class[0]).invoke(anno); + if(null == targetType) { + targetTypes = (Class[])anno.getClass().getMethod("value", new Class[0]).invoke(anno); } else { targetTypes = new Class[]{targetType}; } - for (Class type : targetTypes) { + for(Class type : targetTypes) { handlerMethods.put(eventType, new EventHandlerMethod(type, handler, method)); } - } catch (NoSuchMethodException ignored) { - } catch (InvocationTargetException ignored) { - } catch (IllegalAccessException ignored) { + } catch(NoSuchMethodException ignored) { + } catch(InvocationTargetException ignored) { + } catch(IllegalAccessException ignored) { } } } private class EventHandlerMethod { final Class targetType; - final Method method; - final Object handler; + final Method method; + final Object handler; private EventHandlerMethod(Class targetType, Object handler, diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeDeleteEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeDeleteEvent.java index ee61acedb..5f5c53bed 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeDeleteEvent.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeDeleteEvent.java @@ -5,7 +5,8 @@ package org.springframework.data.rest.repository.context; * * @author Jon Brisbin */ -public class BeforeDeleteEvent extends RepositoryEvent { +public class BeforeDeleteEvent + extends RepositoryEvent { public BeforeDeleteEvent(Object source) { super(source); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeLinkSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeLinkSaveEvent.java index 34991125b..d7c5a45d4 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeLinkSaveEvent.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeLinkSaveEvent.java @@ -5,7 +5,8 @@ package org.springframework.data.rest.repository.context; * * @author Jon Brisbin */ -public class BeforeLinkSaveEvent extends LinkSaveEvent { +public class BeforeLinkSaveEvent + extends LinkSaveEvent { public BeforeLinkSaveEvent(Object source, Object linked) { super(source, linked); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java index ddcfa2408..a5ff5be32 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/BeforeSaveEvent.java @@ -5,7 +5,8 @@ package org.springframework.data.rest.repository.context; * * @author Jon Brisbin */ -public class BeforeSaveEvent extends RepositoryEvent { +public class BeforeSaveEvent + extends RepositoryEvent { public BeforeSaveEvent(Object source) { super(source); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/LinkSaveEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/LinkSaveEvent.java index ea71c7e4a..694a845e0 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/LinkSaveEvent.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/LinkSaveEvent.java @@ -5,7 +5,8 @@ package org.springframework.data.rest.repository.context; * * @author Jon Brisbin */ -public abstract class LinkSaveEvent extends RepositoryEvent { +public abstract class LinkSaveEvent + extends RepositoryEvent { private final Object linked; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoryEvent.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoryEvent.java index f74b15e15..95a1fa101 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoryEvent.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/RepositoryEvent.java @@ -5,7 +5,8 @@ import org.springframework.context.ApplicationEvent; /** * @author Jon Brisbin */ -public abstract class RepositoryEvent extends ApplicationEvent { +public abstract class RepositoryEvent + extends ApplicationEvent { protected RepositoryEvent(Object source) { super(source); } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java index ba0f33ff9..067dd8a27 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/context/ValidatingRepositoryEventListener.java @@ -29,20 +29,21 @@ public class ValidatingRepositoryEventListener private Multimap validators = ArrayListMultimap.create(); - @Override public void afterPropertiesSet() throws Exception { - if (validators.size() == 0) { - for (Map.Entry entry : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, - Validator.class) - .entrySet()) { + @Override public void afterPropertiesSet() + throws Exception { + if(validators.size() == 0) { + for(Map.Entry entry : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, + Validator.class) + .entrySet()) { String name = null; Validator v = entry.getValue(); - if (entry.getKey().contains("Save")) { + if(entry.getKey().contains("Save")) { name = entry.getKey().substring(0, entry.getKey().indexOf("Save") + 4); - } else if (entry.getKey().contains("Delete")) { + } else if(entry.getKey().contains("Delete")) { name = entry.getKey().substring(0, entry.getKey().indexOf("Delete") + 6); } - if (null != name) { + if(null != name) { this.validators.put(name, v); } } @@ -61,11 +62,13 @@ public class ValidatingRepositoryEventListener /** * Assign a Map of {@link Validator}s that are assigned to the various {@link RepositoryEvent}s. * - * @param validators A Map of Validators to wire. + * @param validators + * A Map of Validators to wire. + * * @return @this */ public ValidatingRepositoryEventListener setValidators(Map> validators) { - for (Map.Entry> entry : validators.entrySet()) { + for(Map.Entry> entry : validators.entrySet()) { this.validators.replaceValues(entry.getKey(), entry.getValue()); } return this; @@ -74,8 +77,11 @@ public class ValidatingRepositoryEventListener /** * Add a {@link Validator} that will be triggered on the given event. * - * @param event The event to listen for. - * @param validator The Validator to execute when that event fires. + * @param event + * The event to listen for. + * @param validator + * The Validator to execute when that event fires. + * * @return @this */ public ValidatingRepositoryEventListener addValidator(String event, Validator validator) { @@ -109,21 +115,21 @@ public class ValidatingRepositoryEventListener private Errors validate(String event, Object o) { Errors errors = null; - if (null != o) { + if(null != o) { Class domainType = o.getClass(); errors = new ValidationErrors(domainType.getSimpleName(), o, repositoryMetadataFor(domainType).entityMetadata()); Collection validators = this.validators.get(event); - if (null != validators) { - for (Validator v : validators) { - if (v.supports(o.getClass())) { + if(null != validators) { + for(Validator v : validators) { + if(v.supports(o.getClass())) { LOG.debug(event + ": " + o + " with " + v); ValidationUtils.invokeValidator(v, o, errors); } } } - if (errors.getErrorCount() > 0) { + if(errors.getErrorCount() > 0) { throw new RepositoryConstraintViolationException(errors); } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMethod.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethod.java similarity index 63% rename from spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMethod.java rename to spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethod.java index c91d0c94a..03e134200 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/RepositoryMethod.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethod.java @@ -1,8 +1,7 @@ -package org.springframework.data.rest.repository; +package org.springframework.data.rest.repository.invoke; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.Arrays; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.data.domain.Pageable; @@ -23,16 +22,16 @@ public class RepositoryMethod { FIND_ONE, SAVE; - public static Type fromMethodName( String s ) { - if ( "count".equals( s ) ) { + public static Type fromMethodName(String s) { + if("count".equals(s)) { return COUNT; - } else if ( "delete".equals( s ) ) { + } else if("delete".equals(s)) { return DELETE; - } else if ( "findAll".equals( s ) ) { + } else if("findAll".equals(s)) { return FIND_ALL; - } else if ( "findOne".equals( s ) ) { + } else if("findOne".equals(s)) { return FIND_ONE; - } else if ( "save".equals( s ) ) { + } else if("save".equals(s)) { return SAVE; } else { return CUSTOM; @@ -40,7 +39,7 @@ public class RepositoryMethod { } public String toMethodName() { - switch (this) { + switch(this) { case COUNT: return "count"; case DELETE: @@ -58,49 +57,49 @@ public class RepositoryMethod { } - public static final ReflectionUtils.MethodFilter USER_METHODS = new ReflectionUtils.MethodFilter() { - @Override public boolean matches( Method method ) { + public static final ReflectionUtils.MethodFilter USER_METHODS = new ReflectionUtils.MethodFilter() { + @Override public boolean matches(Method method) { return (!method.isSynthetic() && !method.isBridge() && method.getDeclaringClass() != Object.class - && !method.getName().contains( "$" )); + && !method.getName().contains("$")); } }; public static final LocalVariableTableParameterNameDiscoverer NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer(); - private Method method; + private Method method; private Class[] paramTypes; - private String[] paramNames; + private String[] paramNames; private boolean pageable = false; private boolean sortable = false; - public RepositoryMethod( Method method ) { + public RepositoryMethod(Method method) { this.method = method; paramTypes = method.getParameterTypes(); - for ( Class type : paramTypes ) { - if ( Pageable.class.isAssignableFrom( type ) ) { + for(Class type : paramTypes) { + if(Pageable.class.isAssignableFrom(type)) { pageable = true; } - if ( Sort.class.isAssignableFrom( type ) ) { + if(Sort.class.isAssignableFrom(type)) { sortable = true; } } - paramNames = NAME_DISCOVERER.getParameterNames( method ); - if ( null == paramNames ) { + paramNames = NAME_DISCOVERER.getParameterNames(method); + if(null == paramNames) { paramNames = new String[paramTypes.length]; } Annotation[][] paramAnnos = method.getParameterAnnotations(); - for ( int i = 0; i < paramAnnos.length; i++ ) { - if ( paramAnnos[i].length > 0 ) { - for ( Annotation anno : paramAnnos[i] ) { - if ( Param.class.isAssignableFrom( anno.getClass() ) ) { - Param p = (Param) anno; + for(int i = 0; i < paramAnnos.length; i++) { + if(paramAnnos[i].length > 0) { + for(Annotation anno : paramAnnos[i]) { + if(Param.class.isAssignableFrom(anno.getClass())) { + Param p = (Param)anno; paramNames[i] = p.value(); break; } } } - if ( null == paramNames[i] ) { + if(null == paramNames[i]) { paramNames[i] = "arg" + i; } } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java new file mode 100644 index 000000000..681060054 --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryMethodResponse.java @@ -0,0 +1,103 @@ +package org.springframework.data.rest.repository.invoke; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.codehaus.jackson.annotate.JsonProperty; +import org.springframework.data.rest.core.Link; + +/** + * @author Jon Brisbin + */ +public class RepositoryMethodResponse { + + @JsonProperty("results") + private List results = new ArrayList(); + @JsonProperty("_links") + private List links = new ArrayList(); + private long totalCount = 0; + private int totalPages = 1; + private int currentPage = 1; + + public RepositoryMethodResponse addLink(Link l) { + links.add(l); + return this; + } + + public RepositoryMethodResponse addResult(Object obj) { + results.add(obj); + return this; + } + + public RepositoryMethodResponse addAllResults(Iterator results) { + if(null == results) { + return this; + } + + while(results.hasNext()) { + addResult(results.next()); + } + + return this; + } + + public List getResults() { + return results; + } + + public boolean hasResults() { + return (results.size() > 0); + } + + public RepositoryMethodResponse setResults(List results) { + if(null == results) { + this.results = Collections.emptyList(); + } else { + this.results = results; + } + return this; + } + + public List getLinks() { + return links; + } + + public RepositoryMethodResponse setLinks(List links) { + if(null == links) { + this.links = Collections.emptyList(); + } else { + this.links = links; + } + return this; + } + + public long getTotalCount() { + return totalCount; + } + + public RepositoryMethodResponse setTotalCount(long totalCount) { + this.totalCount = totalCount; + return this; + } + + public int getTotalPages() { + return totalPages; + } + + public RepositoryMethodResponse setTotalPages(int totalPages) { + this.totalPages = totalPages; + return this; + } + + public int getCurrentPage() { + return currentPage; + } + + public RepositoryMethodResponse setCurrentPage(int currentPage) { + this.currentPage = currentPage; + return this; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryQueryMethod.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryQueryMethod.java new file mode 100644 index 000000000..b8cedb95e --- /dev/null +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/invoke/RepositoryQueryMethod.java @@ -0,0 +1,64 @@ +package org.springframework.data.rest.repository.invoke; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.query.Param; +import org.springframework.util.Assert; + +/** + * @author Jon Brisbin + */ +public class RepositoryQueryMethod { + + private Method method; + private Class[] paramTypes; + private String[] paramNames; + + public RepositoryQueryMethod(Method method) { + this.method = method; + paramTypes = method.getParameterTypes(); + paramNames = new String[paramTypes.length]; + if(null == paramNames) { + paramNames = new String[paramTypes.length]; + } + Annotation[][] paramAnnos = method.getParameterAnnotations(); + for(int i = 0; i < paramAnnos.length; i++) { + if(paramAnnos[i].length == 0) { + continue; + } + + for(Annotation anno : paramAnnos[i]) { + if(Param.class.isAssignableFrom(anno.getClass())) { + Param p = (Param)anno; + paramNames[i] = p.value(); + break; + } + } + + if(Pageable.class.isAssignableFrom(paramTypes[i]) + || Sort.class.isAssignableFrom(paramTypes[i])) { + continue; + } + + Assert.notNull(paramNames[i], + "No @Param('name') was provided for parameter " + (i + 1) + " of type " + paramTypes[i] + + " on " + (method.getDeclaringClass().getName() + "." + method.getName())); + } + } + + public Class[] paramTypes() { + return paramTypes; + } + + public String[] paramNames() { + return paramNames; + } + + public Method method() { + return method; + } + +} diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java index 8e3f0bd8d..06362adaa 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaAttributeMetadata.java @@ -17,14 +17,15 @@ import org.springframework.util.ReflectionUtils; /** * @author Jon Brisbin */ -public class JpaAttributeMetadata implements AttributeMetadata { +public class JpaAttributeMetadata + implements AttributeMetadata { - private String name; + private String name; private Attribute attribute; - private Class type; - private Field field; - private Method getter; - private Method setter; + private Class type; + private Field field; + private Method getter; + private Method setter; public JpaAttributeMetadata(EntityType entityType, Attribute attribute) { this.attribute = attribute; @@ -35,14 +36,16 @@ public class JpaAttributeMetadata implements AttributeMetadata { ReflectionUtils.makeAccessible(field); PropertyDescriptor property = BeanUtils.getPropertyDescriptor(entityType.getJavaType(), name); - if (null != property) { + if(null != property) { getter = property.getReadMethod(); - if (null != getter) + if(null != getter) { ReflectionUtils.makeAccessible(getter); + } setter = property.getWriteMethod(); - if (null != setter) + if(null != setter) { ReflectionUtils.makeAccessible(setter); + } } } @@ -56,14 +59,14 @@ public class JpaAttributeMetadata implements AttributeMetadata { @Override public Class elementType() { return (attribute instanceof PluralAttribute - ? ((PluralAttribute) attribute).getElementType().getJavaType() - : null); + ? ((PluralAttribute)attribute).getElementType().getJavaType() + : null); } @Override public boolean isCollectionLike() { - if (attribute instanceof PluralAttribute) { - PluralAttribute plattr = (PluralAttribute) attribute; - switch (plattr.getCollectionType()) { + if(attribute instanceof PluralAttribute) { + PluralAttribute plattr = (PluralAttribute)attribute; + switch(plattr.getCollectionType()) { case COLLECTION: case LIST: return true; @@ -76,13 +79,13 @@ public class JpaAttributeMetadata implements AttributeMetadata { } @Override public Collection asCollection(Object target) { - return (Collection) get(target); + return (Collection)get(target); } @Override public boolean isSetLike() { - if (attribute instanceof PluralAttribute) { - PluralAttribute plattr = (PluralAttribute) attribute; - switch (plattr.getCollectionType()) { + if(attribute instanceof PluralAttribute) { + PluralAttribute plattr = (PluralAttribute)attribute; + switch(plattr.getCollectionType()) { case SET: return true; default: @@ -94,13 +97,13 @@ public class JpaAttributeMetadata implements AttributeMetadata { } @Override public Set asSet(Object target) { - return (Set) get(target); + return (Set)get(target); } @Override public boolean isMapLike() { - if (attribute instanceof PluralAttribute) { - PluralAttribute plattr = (PluralAttribute) attribute; - switch (plattr.getCollectionType()) { + if(attribute instanceof PluralAttribute) { + PluralAttribute plattr = (PluralAttribute)attribute; + switch(plattr.getCollectionType()) { case MAP: return true; default: @@ -112,29 +115,29 @@ public class JpaAttributeMetadata implements AttributeMetadata { } @Override public Map asMap(Object target) { - return (Map) get(target); + return (Map)get(target); } @Override public Object get(Object target) { try { - if (null != getter) { + if(null != getter) { return getter.invoke(target); } else { return field.get(target); } - } catch (Exception e) { + } catch(Exception e) { return null; } } @Override public AttributeMetadata set(Object value, Object target) { try { - if (null != setter) { + if(null != setter) { setter.invoke(target, value); } else { field.set(target, value); } - } catch (Exception e) { + } catch(Exception e) { } return this; } diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaEntityMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaEntityMetadata.java index 90ff4e5dc..fdf234a0d 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaEntityMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaEntityMetadata.java @@ -16,40 +16,41 @@ import org.springframework.util.ReflectionUtils; /** * @author Jon Brisbin */ -public class JpaEntityMetadata implements EntityMetadata { +public class JpaEntityMetadata + implements EntityMetadata { - private Class type; + private Class type; private JpaAttributeMetadata idAttribute; private JpaAttributeMetadata versionAttribute; private Map embeddedAttributes = new HashMap(); - private Map linkedAttributes = new HashMap(); + private Map linkedAttributes = new HashMap(); @SuppressWarnings({"unchecked"}) public JpaEntityMetadata(Repositories repositories, EntityType entityType) { type = entityType.getJavaType(); idAttribute = new JpaAttributeMetadata(entityType, entityType.getId(entityType.getIdType().getJavaType())); - if (null != entityType.getVersion(Long.class)) { + if(null != entityType.getVersion(Long.class)) { versionAttribute = new JpaAttributeMetadata(entityType, entityType.getVersion(Long.class)); } - for (Attribute attr : entityType.getAttributes()) { + for(Attribute attr : entityType.getAttributes()) { boolean exported = true; Field field = ReflectionUtils.findField(type, attr.getJavaMember().getName()); - if (null != field) { + if(null != field) { RestResource fieldResourceAnno = field.getAnnotation(RestResource.class); - if (null != fieldResourceAnno) { + if(null != fieldResourceAnno) { exported = fieldResourceAnno.exported(); } } - if (exported) { + if(exported) { Class attrType = (attr instanceof PluralAttribute - ? ((PluralAttribute) attr).getElementType().getJavaType() - : attr.getJavaType()); - if (repositories.hasRepositoryFor(attrType)) { + ? ((PluralAttribute)attr).getElementType().getJavaType() + : attr.getJavaType()); + if(repositories.hasRepositoryFor(attrType)) { linkedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr)); } else { - if (!(attr instanceof SingularAttribute && ((SingularAttribute) attr).isId()) - && !(attr instanceof SingularAttribute && ((SingularAttribute) attr).isVersion())) { + if(!(attr instanceof SingularAttribute && ((SingularAttribute)attr).isId()) + && !(attr instanceof SingularAttribute && ((SingularAttribute)attr).isVersion())) { embeddedAttributes.put(attr.getName(), new JpaAttributeMetadata(entityType, attr)); } } @@ -78,13 +79,13 @@ public class JpaEntityMetadata implements EntityMetadata { } @Override public JpaAttributeMetadata attribute(String name) { - if (idAttribute.name().equals(name)) { + if(idAttribute.name().equals(name)) { return idAttribute; - } else if (null != versionAttribute && versionAttribute.name().equals(name)) { + } else if(null != versionAttribute && versionAttribute.name().equals(name)) { return versionAttribute; - } else if (embeddedAttributes.containsKey(name)) { + } else if(embeddedAttributes.containsKey(name)) { return embeddedAttributes.get(name); - } else if (linkedAttributes.containsKey(name)) { + } else if(linkedAttributes.containsKey(name)) { return linkedAttributes.get(name); } return null; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryExporter.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryExporter.java index b43ff5e2f..9e523ca45 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryExporter.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryExporter.java @@ -12,7 +12,8 @@ import org.springframework.data.rest.repository.RepositoryExporter; * * @author Jon Brisbin */ -public class JpaRepositoryExporter extends RepositoryExporter { +public class JpaRepositoryExporter + extends RepositoryExporter { protected EntityManager entityManager; diff --git a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryMetadata.java b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryMetadata.java index 52fab5ad2..1dfe91846 100644 --- a/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryMetadata.java +++ b/spring-data-rest-repository/src/main/java/org/springframework/data/rest/repository/jpa/JpaRepositoryMetadata.java @@ -12,23 +12,24 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.repository.RepositoryMetadata; -import org.springframework.data.rest.repository.RepositoryQueryMethod; import org.springframework.data.rest.repository.annotation.RestResource; +import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** * @author Jon Brisbin */ -public class JpaRepositoryMetadata implements RepositoryMetadata { +public class JpaRepositoryMetadata + implements RepositoryMetadata { - private final String name; - private final Class repoClass; + private final String name; + private final Class repoClass; private final CrudRepository repository; - private final EntityInformation entityInfo; + private final EntityInformation entityInfo; private final Map queryMethods = new HashMap(); - private String rel; + private String rel; private JpaEntityMetadata entityMetadata; @SuppressWarnings({"unchecked"}) @@ -43,23 +44,23 @@ public class JpaRepositoryMetadata implements RepositoryMetadata */ -@ContextConfiguration(locations = ["/JpaMetadataSpec-test.xml"]) +@ContextConfiguration(classes = [ApplicationConfig]) class JpaMetadataSpec extends Specification { @Autowired diff --git a/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java new file mode 100644 index 000000000..379192579 --- /dev/null +++ b/spring-data-rest-repository/src/test/java/org/springframework/data/rest/repository/test/ApplicationConfig.java @@ -0,0 +1,66 @@ +package org.springframework.data.rest.repository.test; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.rest.repository.jpa.JpaRepositoryExporter; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.orm.jpa.JpaDialect; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaDialect; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Jon Brisbin + */ +@Configuration +@ComponentScan(basePackageClasses = ApplicationConfig.class) +@EnableJpaRepositories +@EnableTransactionManagement +public class ApplicationConfig { + + @Bean public DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + + @Bean public EntityManagerFactory entityManagerFactory() { + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setDatabase(Database.HSQL); + vendorAdapter.setGenerateDdl(true); + + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setJpaVendorAdapter(vendorAdapter); + factory.setPackagesToScan(getClass().getPackage().getName()); + factory.setDataSource(dataSource()); + factory.setPersistenceXmlLocation("/JpaMetadataSpec-persistence.xml"); + + factory.afterPropertiesSet(); + + return factory.getObject(); + } + + @Bean public JpaDialect jpaDialect() { + return new HibernateJpaDialect(); + } + + @Bean public PlatformTransactionManager transactionManager() { + JpaTransactionManager txManager = new JpaTransactionManager(); + txManager.setEntityManagerFactory(entityManagerFactory()); + return txManager; + } + + @Bean public JpaRepositoryExporter jpaRepositoryExporter() { + return new JpaRepositoryExporter(); + } + +} diff --git a/spring-data-rest-webmvc/build.gradle b/spring-data-rest-webmvc/build.gradle index 390f5af77..330203430 100644 --- a/spring-data-rest-webmvc/build.gradle +++ b/spring-data-rest-webmvc/build.gradle @@ -1,7 +1,14 @@ +apply plugin: "war" +apply plugin: "jetty" + +jettyRun { + contextPath = "" +} + dependencies { // APIS - compile "javax.servlet:servlet-api:2.5" + compile "javax.servlet:javax.servlet-api:3.0.1" // JPA compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.1.Final" @@ -12,7 +19,6 @@ dependencies { // Spring compile "org.springframework:spring-webmvc:$springVersion" - runtime "cglib:cglib-nodep:2.2.2" // Repository Exporter support compile project(":spring-data-rest-repository") diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/JacksonUtil.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/JacksonUtil.java index b2077cfae..bbca8d00c 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/JacksonUtil.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/JacksonUtil.java @@ -1,7 +1,6 @@ package org.springframework.data.rest.webmvc; import java.io.IOException; -import java.nio.charset.Charset; import java.util.Arrays; import org.codehaus.jackson.JsonEncoding; @@ -16,51 +15,50 @@ import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; /** + * Utility class for creating a custom-configured {@see MappingJacksonHttpMessageConverter} that has our own + * serializers and {@see MediaType} mappings on it. + * * @author Jon Brisbin */ public abstract class JacksonUtil { - public static final Charset DEFAULT_CHARSET = Charset.forName( "UTF-8" ); - public static final MediaType COMPACT_JSON = new MediaType( "application", - "x-spring-data-compact+json", - DEFAULT_CHARSET ); - public static final MediaType VERBOSE_JSON = new MediaType( "application", - "x-spring-data-verbose+json", - DEFAULT_CHARSET ); - public static final MediaType APPLICATION_JAVASCRIPT = new MediaType( "application", - "javascript", - DEFAULT_CHARSET ); - private JacksonUtil() { } - public static MappingJacksonHttpMessageConverter createJacksonHttpMessageConverter( final ObjectMapper objectMapper ) { + public static MappingJacksonHttpMessageConverter createJacksonHttpMessageConverter(final ObjectMapper objectMapper) { + // We need a custom serializer for handling beans that don't conform to the JavaBeans standard 'get' and 'set' CustomSerializerFactory customSerializerFactory = new CustomSerializerFactory(); - customSerializerFactory.addSpecificMapping( SimpleLink.class, new FluentBeanSerializer( SimpleLink.class ) ); - objectMapper.setSerializerFactory( customSerializerFactory ); + customSerializerFactory.addSpecificMapping(SimpleLink.class, new FluentBeanSerializer(SimpleLink.class)); + objectMapper.setSerializerFactory(customSerializerFactory); + // We want to support all our custom types of JSON and also the catch-all MappingJacksonHttpMessageConverter jsonConverter = new MappingJacksonHttpMessageConverter() { { - setSupportedMediaTypes( Arrays.asList( MediaType.APPLICATION_JSON, COMPACT_JSON, VERBOSE_JSON ) ); + setSupportedMediaTypes(Arrays.asList( + MediaType.APPLICATION_JSON, + MediaTypes.COMPACT_JSON, + MediaTypes.VERBOSE_JSON, + MediaType.ALL + )); } @Override - protected void writeInternal( Object object, HttpOutputMessage outputMessage ) + protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { - JsonEncoding encoding = getJsonEncoding( outputMessage.getHeaders().getContentType() ); + JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); // Believe it or not, this is the only way to get pretty-printing from Jackson in this configuration JsonGenerator jsonGenerator = objectMapper .getJsonFactory() - .createJsonGenerator( outputMessage.getBody(), encoding ) + .createJsonGenerator(outputMessage.getBody(), encoding) .useDefaultPrettyPrinter(); try { - objectMapper.writeValue( jsonGenerator, object ); - } catch ( IOException ex ) { - throw new HttpMessageNotWritableException( "Could not write JSON: " + ex.getMessage(), ex ); + objectMapper.writeValue(jsonGenerator, object); + } catch(IOException ex) { + throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); } } }; - jsonConverter.setObjectMapper( objectMapper ); + jsonConverter.setObjectMapper(objectMapper); return jsonConverter; } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/Links.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/Links.java deleted file mode 100644 index 5b02e155d..000000000 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/Links.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.springframework.data.rest.webmvc; - -import java.util.ArrayList; -import java.util.List; - -import org.codehaus.jackson.annotate.JsonProperty; -import org.codehaus.jackson.map.annotate.JsonDeserialize; -import org.springframework.data.rest.core.Link; -import org.springframework.data.rest.core.SimpleLink; - -/** - * @author Jon Brisbin - */ -public class Links { - - private List links = new ArrayList(); - - public Links add(SimpleLink link) { - links.add(link); - return this; - } - - @JsonProperty("_links") - public List getLinks() { - return this.links; - } - -} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/MediaTypes.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/MediaTypes.java new file mode 100644 index 000000000..f7bf284d7 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/MediaTypes.java @@ -0,0 +1,33 @@ +package org.springframework.data.rest.webmvc; + +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; + +import org.springframework.http.MediaType; + +/** + * @author Jon Brisbin + */ +public abstract class MediaTypes { + + private MediaTypes() { + } + + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + + public static final List ACCEPT_ALL_TYPES = Collections.singletonList(MediaType.ALL); + public static final MediaType COMPACT_JSON = new MediaType("application", + "x-spring-data-compact+json", + ISO_8859_1); + public static final MediaType VERBOSE_JSON = new MediaType("application", + "x-spring-data-verbose+json", + ISO_8859_1); + public static final MediaType APPLICATION_JAVASCRIPT = new MediaType("application", + "javascript", + ISO_8859_1); + public static final MediaType URI_LIST = new MediaType("text", + "uri-list", + ISO_8859_1); + +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java index b69cc4117..224d66653 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSorting.java @@ -10,12 +10,15 @@ import org.springframework.data.domain.Sort; import org.springframework.web.util.UriComponentsBuilder; /** + * Implementation of {@link Pageable} that is URL-aware. + * * @author Jon Brisbin */ -public class PagingAndSorting implements Pageable { +public class PagingAndSorting + implements Pageable { private final RepositoryRestConfiguration config; - private final PageRequest pageRequest; + private final PageRequest pageRequest; public PagingAndSorting(RepositoryRestConfiguration config, PageRequest pageRequest) { @@ -23,17 +26,24 @@ public class PagingAndSorting implements Pageable { this.pageRequest = pageRequest; } + /** + * Add the current sort parameters to the URI. + * + * @param urib + * + * @return + */ public PagingAndSorting addSortParameters(UriComponentsBuilder urib) { Sort sort = pageRequest.getSort(); - if (null != sort) { + if(null != sort) { Iterator iter = sort.iterator(); - while (iter.hasNext()) { + while(iter.hasNext()) { Sort.Order order = iter.next(); urib.queryParam(config.getSortParamName(), order.getProperty()); try { urib.queryParam(URLEncoder.encode(order.getProperty() + ".dir", "ISO-8859-1"), order.getDirection().toString().toLowerCase()); - } catch (UnsupportedEncodingException ignored) { + } catch(UnsupportedEncodingException ignored) { // this should never happen } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java index e1163cd8c..10a086a09 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/PagingAndSortingMethodArgumentResolver.java @@ -5,11 +5,11 @@ import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.lang.ClassUtils; import org.springframework.core.MethodParameter; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefaults; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; @@ -19,14 +19,15 @@ import org.springframework.web.method.support.ModelAndViewContainer; /** * @author Jon Brisbin */ -public class PagingAndSortingMethodArgumentResolver implements HandlerMethodArgumentResolver { +public class PagingAndSortingMethodArgumentResolver + implements HandlerMethodArgumentResolver { private static final int DEFAULT_PAGE = 1; // We're 1-based, not 0-based private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; public PagingAndSortingMethodArgumentResolver(RepositoryRestConfiguration config) { - if (null != config) { + if(null != config) { this.config = config; } } @@ -40,47 +41,49 @@ public class PagingAndSortingMethodArgumentResolver implements HandlerMethodArgu ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(); PageRequest pr = null; - for (Annotation annotation : parameter.getParameterAnnotations()) { - if (annotation instanceof PageableDefaults) { - PageableDefaults defaults = (PageableDefaults) annotation; + for(Annotation annotation : parameter.getParameterAnnotations()) { + if(annotation instanceof PageableDefaults) { + PageableDefaults defaults = (PageableDefaults)annotation; pr = new PageRequest(defaults.pageNumber(), defaults.value()); break; } } - if (null == pr) { + if(null == pr) { int page = DEFAULT_PAGE; String sPage = request.getParameter(config.getPageParamName()); - if (StringUtils.hasText(sPage)) { + if(StringUtils.hasText(sPage)) { try { page = Integer.parseInt(sPage); - } catch (NumberFormatException ignored) {} + } catch(NumberFormatException ignored) { + } } int limit = config.getDefaultPageSize(); String sLimit = request.getParameter(config.getLimitParamName()); - if (StringUtils.hasText(sLimit)) { + if(StringUtils.hasText(sLimit)) { try { limit = Integer.parseInt(sLimit); - } catch (NumberFormatException ignored) {} + } catch(NumberFormatException ignored) { + } } Sort sort = null; List orders = new ArrayList(); String[] orderValues = request.getParameterValues(config.getSortParamName()); - if (null != orderValues) { - for (String orderParam : orderValues) { + if(null != orderValues) { + for(String orderParam : orderValues) { String sortDir = request.getParameter(orderParam + ".dir"); Sort.Direction dir = (null != sortDir ? Sort.Direction.valueOf(sortDir.toUpperCase()) : Sort.Direction.ASC); orders.add(new Sort.Order(dir, orderParam)); } - if (!orders.isEmpty()) { + if(!orders.isEmpty()) { sort = new Sort(orders); } } - if (null != sort) { + if(null != sort) { pr = new PageRequest(page - 1, limit, sort); } else { pr = new PageRequest(page - 1, limit); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java index b708fe713..ea7948ddc 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestConfiguration.java @@ -3,6 +3,7 @@ package org.springframework.data.rest.webmvc; import java.util.Collections; import java.util.List; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; /** @@ -12,19 +13,20 @@ public class RepositoryRestConfiguration { public static final RepositoryRestConfiguration DEFAULT = new RepositoryRestConfiguration(); - private int defaultPageSize = 20; - private String pageParamName = "page"; - private String limitParamName = "limit"; - private String sortParamName = "sort"; - private String jsonpParamName = "callback"; - private String jsonpOnErrParamName = null; - private List> customConverters = Collections.emptyList(); + private int defaultPageSize = 20; + private String pageParamName = "page"; + private String limitParamName = "limit"; + private String sortParamName = "sort"; + private String jsonpParamName = "callback"; + private String jsonpOnErrParamName = null; + private List> customConverters = Collections.emptyList(); + private MediaType defaultMediaType = MediaType.APPLICATION_JSON; public int getDefaultPageSize() { return defaultPageSize; } - public RepositoryRestConfiguration setDefaultPageSize( int defaultPageSize ) { + public RepositoryRestConfiguration setDefaultPageSize(int defaultPageSize) { this.defaultPageSize = defaultPageSize; return this; } @@ -33,7 +35,7 @@ public class RepositoryRestConfiguration { return pageParamName; } - public RepositoryRestConfiguration setPageParamName( String pageParamName ) { + public RepositoryRestConfiguration setPageParamName(String pageParamName) { this.pageParamName = pageParamName; return this; } @@ -42,7 +44,7 @@ public class RepositoryRestConfiguration { return limitParamName; } - public RepositoryRestConfiguration setLimitParamName( String limitParamName ) { + public RepositoryRestConfiguration setLimitParamName(String limitParamName) { this.limitParamName = limitParamName; return this; } @@ -51,7 +53,7 @@ public class RepositoryRestConfiguration { return sortParamName; } - public RepositoryRestConfiguration setSortParamName( String sortParamName ) { + public RepositoryRestConfiguration setSortParamName(String sortParamName) { this.sortParamName = sortParamName; return this; } @@ -60,7 +62,7 @@ public class RepositoryRestConfiguration { return customConverters; } - public RepositoryRestConfiguration setCustomConverters( List> customConverters ) { + public RepositoryRestConfiguration setCustomConverters(List> customConverters) { this.customConverters = customConverters; return this; } @@ -69,7 +71,7 @@ public class RepositoryRestConfiguration { return jsonpParamName; } - public RepositoryRestConfiguration setJsonpParamName( String jsonpParamName ) { + public RepositoryRestConfiguration setJsonpParamName(String jsonpParamName) { this.jsonpParamName = jsonpParamName; return this; } @@ -78,9 +80,18 @@ public class RepositoryRestConfiguration { return jsonpOnErrParamName; } - public RepositoryRestConfiguration setJsonpOnErrParamName( String jsonpOnErrParamName ) { + public RepositoryRestConfiguration setJsonpOnErrParamName(String jsonpOnErrParamName) { this.jsonpOnErrParamName = jsonpOnErrParamName; return this; } + public MediaType getDefaultMediaType() { + return defaultMediaType; + } + + public RepositoryRestConfiguration setDefaultMediaType(MediaType defaultMediaType) { + this.defaultMediaType = defaultMediaType; + return this; + } + } 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 11412f7e1..09b7ed8f7 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 @@ -1,16 +1,13 @@ package org.springframework.data.rest.webmvc; -import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.URI; -import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -23,7 +20,6 @@ import java.util.SortedSet; import java.util.Stack; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicReference; -import javax.servlet.http.HttpServletRequest; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; @@ -44,6 +40,7 @@ import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.Repository; import org.springframework.data.rest.core.Handler; import org.springframework.data.rest.core.Link; +import org.springframework.data.rest.core.Links; import org.springframework.data.rest.core.SimpleLink; import org.springframework.data.rest.core.convert.DelegatingConversionService; import org.springframework.data.rest.core.util.UriUtils; @@ -54,7 +51,6 @@ import org.springframework.data.rest.repository.RepositoryExporter; 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.RepositoryQueryMethod; import org.springframework.data.rest.repository.annotation.RestResource; import org.springframework.data.rest.repository.context.AfterDeleteEvent; import org.springframework.data.rest.repository.context.AfterLinkSaveEvent; @@ -62,6 +58,9 @@ import org.springframework.data.rest.repository.context.AfterSaveEvent; import org.springframework.data.rest.repository.context.BeforeDeleteEvent; import org.springframework.data.rest.repository.context.BeforeLinkSaveEvent; import org.springframework.data.rest.repository.context.BeforeSaveEvent; +import org.springframework.data.rest.repository.context.RepositoryEvent; +import org.springframework.data.rest.repository.invoke.RepositoryMethodResponse; +import org.springframework.data.rest.repository.invoke.RepositoryQueryMethod; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; @@ -74,6 +73,7 @@ import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.Assert; @@ -88,7 +88,34 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.util.UriComponentsBuilder; /** - * @author Jon Brisbin + * Exports Spring Data Repositories over the web in a RESTful + * manner that is HATEOAS friendly. + *

+ * This controller can be deployed in it's own DispatcherServlet. In that case, use the {@link + * RepositoryRestExporterServlet} in your web.xml. For example, to send all requests through the REST exporter, add the + * following to your web.xml: + *

+ *

<servlet>
+ *   <servlet-name>exporter</servlet-name>
+ *   <servlet-class>org.springframework.data.rest.webmvc.RepositoryRestExporterServlet</servlet-class>
+ *   <load-on-startup>1</load-on-startup>
+ * </servlet>
+ * 

+ * <servlet-mapping> + * <servlet-name>exporter</servlet-name> + * <url-pattern>/*</url-pattern> + * </servlet-mapping> + *

+ *

+ * One can also deploy this controller into an existing Spring MVC application. In general, one should be able to + * simply create an instance of the {@link RepositoryRestMvcConfiguration} bean in your ApplicationContext + * or in JavaConfig. + *

+ * If you wish to alter the way the REST exporter functions, you don't configure the controller directly. Instead there + * is a {@link RepositoryRestConfiguration} helper class that you create in your ApplicationContext. If a feature is + * configurable in Spring Data REST, there is a property on this helper to configure it. + * + * @author Jon Brisbin */ public class RepositoryRestController extends RepositoryExporterSupport @@ -96,217 +123,255 @@ public class RepositoryRestController InitializingBean { public static final String LOCATION = "Location"; - public static final String SELF = "self"; - public static final String LINKS = "_links"; - public static final Charset DEFAULT_CHARSET = Charset.forName( "UTF-8" ); + public static final String SELF = "self"; + public static final String LINKS = "_links"; - private static final Logger LOG = LoggerFactory.getLogger( RepositoryRestController.class ); - private static final HttpHeaders EMPTY_HEADERS = new HttpHeaders(); - private static final List ALL_TYPES = Arrays.asList( MediaType.ALL ); - private static final MediaType URI_LIST = new MediaType( "text", - "uri-list", - UriListHttpMessageConverter.DEFAULT_CHARSET ); + private static final Logger LOG = LoggerFactory.getLogger(RepositoryRestController.class); private ApplicationContext applicationContext; + /** + * 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. + */ @Autowired(required = false) - private DelegatingConversionService conversionService = new DelegatingConversionService( + private DelegatingConversionService conversionService = new DelegatingConversionService( new DefaultFormattingConversionService() ); + /** + * Converters for reading and writing representations of objects. + */ @Autowired(required = false) - private List httpMessageConverters = new ArrayList(); - private SortedSet availableMediaTypes = new TreeSet(); + 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(); @Autowired(required = false) - private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; - private Map> resourceHandlers = Collections.emptyMap(); - private ObjectMapper objectMapper = new ObjectMapper(); + private RepositoryRestConfiguration config = RepositoryRestConfiguration.DEFAULT; + private ObjectMapper objectMapper = new ObjectMapper(); { List httpMessageConverters = new ArrayList(); - httpMessageConverters.add( 0, new StringHttpMessageConverter() ); - httpMessageConverters.add( 0, new ByteArrayHttpMessageConverter() ); - httpMessageConverters.add( 0, new FormHttpMessageConverter() ); - httpMessageConverters.add( 0, new UriListHttpMessageConverter() ); - httpMessageConverters.add( 0, JacksonUtil.createJacksonHttpMessageConverter( objectMapper ) ); + httpMessageConverters.add(0, new StringHttpMessageConverter()); + httpMessageConverters.add(0, new ByteArrayHttpMessageConverter()); + httpMessageConverters.add(0, new FormHttpMessageConverter()); + httpMessageConverters.add(0, JacksonUtil.createJacksonHttpMessageConverter(objectMapper)); + httpMessageConverters.add(0, new UriListHttpMessageConverter()); - setHttpMessageConverters( httpMessageConverters ); + setHttpMessageConverters(httpMessageConverters); } - @Override public void setApplicationContext( ApplicationContext applicationContext ) throws BeansException { + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } + /** + * Get the {@link ConversionService} in use by the controller. + * + * @return The internal {@link ConversionService}. + */ public ConversionService getConversionService() { return conversionService; } - public void setConversionService( ConversionService conversionService ) { - if ( null != conversionService ) { - this.conversionService.addConversionServices( conversionService ); + /** + * Add this {@link ConversionService} to the list of those being delegated to by the internal {@link + * DelegatingConversionService}. Although this method does an 'add', it is called 'set' to make it JavaBean-friendly. + * + * @param conversionService + */ + public void setConversionService(ConversionService conversionService) { + if(null != conversionService) { + this.conversionService.addConversionServices(conversionService); } } + /** + * @return The internal {@link ConversionService}. + * + * @see org.springframework.data.rest.webmvc.RepositoryRestController#getConversionService() + */ public ConversionService conversionService() { return conversionService; } - public RepositoryRestController conversionService( ConversionService conversionService ) { - setConversionService( conversionService ); + /** + * @param conversionService + * + * @return @this + * + * @see RepositoryRestController#setConversionService(org.springframework.core.convert.ConversionService) + */ + public RepositoryRestController conversionService(ConversionService conversionService) { + setConversionService(conversionService); return this; } + /** + * Get the list of default {@link HttpMessageConverter}s. + * + * @return Default converters. + */ public List getHttpMessageConverters() { return httpMessageConverters; } + /** + * Set the list of available {@link HttpMessageConverter}s, clobbering the defaults. This does not, however, affect + * those user-defined converters that come from the {@link RepositoryRestConfiguration}. + * + * @param httpMessageConverters + */ @SuppressWarnings({"unchecked"}) - public void setHttpMessageConverters( List httpMessageConverters ) { - Assert.notNull( httpMessageConverters ); + public void setHttpMessageConverters(List httpMessageConverters) { + Assert.notNull(httpMessageConverters); this.httpMessageConverters = httpMessageConverters; this.availableMediaTypes.clear(); - for ( HttpMessageConverter conv : httpMessageConverters ) { - availableMediaTypes.addAll( conv.getSupportedMediaTypes() ); + for(HttpMessageConverter conv : httpMessageConverters) { + availableMediaTypes.addAll(conv.getSupportedMediaTypes()); } - for ( HttpMessageConverter conv : config.getCustomConverters() ) { - availableMediaTypes.addAll( conv.getSupportedMediaTypes() ); + for(HttpMessageConverter conv : config.getCustomConverters()) { + availableMediaTypes.addAll(conv.getSupportedMediaTypes()); } } + /** + * @return @this + * + * @see org.springframework.data.rest.webmvc.RepositoryRestController#getHttpMessageConverters() + */ public List httpMessageConverters() { return httpMessageConverters; } - public RepositoryRestController httpMessageConverters( List httpMessageConverters ) { - setHttpMessageConverters( httpMessageConverters ); + /** + * @param httpMessageConverters + * + * @return @this + * + * @see RepositoryRestController#setHttpMessageConverters(java.util.List) + */ + public RepositoryRestController httpMessageConverters(List httpMessageConverters) { + setHttpMessageConverters(httpMessageConverters); return this; } + /** + * Get the configuration currently in use. + * + * @return Either the user-defined configuration or a default. + */ public RepositoryRestConfiguration getRepositoryRestConfig() { return config; } - public RepositoryRestController setRepositoryRestConfig( RepositoryRestConfiguration config ) { + /** + * Set the configuration this controller will use to inflence its behavior. + * + * @param config + * + * @return @this + */ + public RepositoryRestController setRepositoryRestConfig(RepositoryRestConfiguration config) { this.config = config; return this; } - public Map> getResourceHandlers() { - return resourceHandlers; - } - - public RepositoryRestController setResourceHandlers( Map> resourceHandlers ) { - this.resourceHandlers = resourceHandlers; - return this; - } - - public Map> resourceHandlers() { - return resourceHandlers; - } - - public RepositoryRestController resourceHandlers( Map> resourceHandlers ) { - setResourceHandlers( resourceHandlers ); - return this; - } - @SuppressWarnings({"unchecked"}) @Override public void afterPropertiesSet() throws Exception { - for ( ConversionService convsvc : BeanFactoryUtils.beansOfTypeIncludingAncestors( applicationContext, - ConversionService.class ) - .values() ) { - conversionService.addConversionServices( convsvc ); + for(ConversionService convsvc : BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, + ConversionService.class) + .values()) { + conversionService.addConversionServices(convsvc); } } + /** + * List available {@link CrudRepository}s that are being exported. + * + * @param request + * @param uriBuilder + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/", method = RequestMethod.GET ) @ResponseBody - public ResponseEntity listRepositories( ServletServerHttpRequest request, - UriComponentsBuilder uriBuilder ) - throws IOException { + public ResponseEntity listRepositories(ServletServerHttpRequest request, + UriComponentsBuilder uriBuilder) throws IOException { URI baseUri = uriBuilder.build().toUri(); Links links = new Links(); - for ( RepositoryExporter repoExporter : repositoryExporters ) { - for ( String name : (Set) repoExporter.repositoryNames() ) { - RepositoryMetadata repoMeta = repoExporter.repositoryMetadataFor( name ); + for(RepositoryExporter repoExporter : repositoryExporters) { + for(String name : (Set)repoExporter.repositoryNames()) { + RepositoryMetadata repoMeta = repoExporter.repositoryMetadataFor(name); String rel = repoMeta.rel(); - URI path = buildUri( baseUri, name ); - links.add( new SimpleLink( rel, path ) ); + URI path = buildUri(baseUri, name); + links.add(new SimpleLink(rel, path)); } } - return negotiateResponse( request, HttpStatus.OK, EMPTY_HEADERS, links ); + return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), links); } + /** + * List entities of a {@link CrudRepository} by invoking + * {@link org.springframework.data.repository.CrudRepository#findAll()} + * and applying any available paging parameters. + * + * @param request + * @param pageSort + * @param uriBuilder + * @param repository + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}", method = RequestMethod.GET ) @ResponseBody - public ResponseEntity listEntities( ServletServerHttpRequest request, - PagingAndSorting pageSort, - UriComponentsBuilder uriBuilder, - @PathVariable String repository ) - throws IOException { + public ResponseEntity listEntities(ServletServerHttpRequest request, + PagingAndSorting pageSort, + UriComponentsBuilder uriBuilder, + @PathVariable String repository) throws IOException { URI baseUri = uriBuilder.build().toUri(); - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); + RepositoryMethodResponse response = new RepositoryMethodResponse(); - Page page = null; - Iterator iter; - if ( repoMeta.repository() instanceof PagingAndSortingRepository ) { - page = ((PagingAndSortingRepository) repoMeta.repository()).findAll( pageSort ); - iter = page.iterator(); - } else { - iter = repoMeta.repository().findAll().iterator(); - } - - Map resultMap = new HashMap(); - Links links = new Links(); - resultMap.put( LINKS, links.getLinks() ); - List resultList = new ArrayList(); - resultMap.put( "results", resultList ); - - boolean returnLinks = shouldReturnLinks( request.getServletRequest().getHeader( "Accept" ) ); - if ( null != iter ) { - while ( iter.hasNext() ) { - Object o = iter.next(); - Serializable id = (Serializable) repoMeta.entityMetadata().idAttribute().get( o ); - if ( returnLinks ) { - links.add( new SimpleLink( repoMeta.rel() + "." + o.getClass().getSimpleName(), - buildUri( baseUri, repository, id.toString() ) ) ); - } else { - Map entityDto = extractPropertiesLinkAware( repoMeta.rel(), - o, - repoMeta.entityMetadata(), - buildUri( baseUri, repository, id.toString() ) ); - addSelfLink( baseUri, entityDto, repository, id.toString() ); - resultList.add( entityDto ); - } + Iterator allEntities = Collections.emptyList().iterator(); + if(repoMeta.repository() instanceof PagingAndSortingRepository) { + Page page = ((PagingAndSortingRepository)repoMeta.repository()).findAll(pageSort); + if(page.hasContent()) { + allEntities = page.iterator(); } - links.add( new SimpleLink( repoMeta.rel() + ".search", - buildUri( baseUri, repository, "search" ) ) ); - } - // Add paging links - if ( null != page ) { - resultMap.put( "totalCount", page.getTotalElements() ); - resultMap.put( "totalPages", page.getTotalPages() ); - resultMap.put( "currentPage", page.getNumber() + 1 ); + // Set page counts in the response + response.setTotalCount(page.getTotalElements()); + response.setTotalPages(page.getTotalPages()); + response.setCurrentPage(page.getNumber() + 1); + // Copy over parameters - UriComponentsBuilder urib = UriComponentsBuilder.fromUri( baseUri ).pathSegment( repository ); - for ( String name : ((Map) request.getServletRequest().getParameterMap()).keySet() ) { - if ( !config.getPageParamName().equals( name ) && !config.getLimitParamName().equals( name ) - && !config.getSortParamName().equals( name ) ) { - urib.queryParam( name, request.getServletRequest().getParameter( name ) ); + UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository); + for(String name : request.getServletRequest().getParameterMap().keySet()) { + if(notPagingParam(name)) { + selfUri.queryParam(name, request.getServletRequest().getParameter(name)); } } - URI nextPrevBase = urib.build().toUri(); + // Add next/prev links as necessary + URI nextPrevBase = selfUri.build().toUri(); maybeAddPrevNextLink( nextPrevBase, repoMeta, @@ -315,7 +380,7 @@ public class RepositoryRestController !page.isFirstPage() && page.hasPreviousPage(), page.getNumber(), "prev", - links + response.getLinks() ); maybeAddPrevNextLink( nextPrevBase, @@ -325,157 +390,204 @@ public class RepositoryRestController !page.isLastPage() && page.hasNextPage(), page.getNumber() + 2, "next", - links + response.getLinks() ); + + } else { + Iterable it = repoMeta.repository().findAll(); + if(null != it) { + allEntities = it.iterator(); + } } - return negotiateResponse( request, HttpStatus.OK, EMPTY_HEADERS, resultMap ); + while(allEntities.hasNext()) { + Object o = allEntities.next(); + Serializable id = (Serializable)repoMeta.entityMetadata().idAttribute().get(o); + if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) { + response.addLink(new SimpleLink(repoMeta.rel() + "." + o.getClass().getSimpleName(), + buildUri(baseUri, repository, id.toString()))); + } else { + Map entityDto = extractPropertiesLinkAware(repoMeta.rel(), + o, + repoMeta.entityMetadata(), + buildUri(baseUri, repository, id.toString())); + addSelfLink(baseUri, entityDto, repository, id.toString()); + response.addResult(entityDto); + } + } + response.addLink(new SimpleLink(repoMeta.rel() + ".search", + buildUri(baseUri, repository, "search"))); + + return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), response); } + /** + * List the URIs of query methods found on this repository interface. + * + * @param request + * @param uriBuilder + * @param repository + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/search", method = RequestMethod.GET ) @ResponseBody - public ResponseEntity listQueryMethods( ServletServerHttpRequest request, - UriComponentsBuilder uriBuilder, - @PathVariable String repository ) - throws IOException { + public ResponseEntity listQueryMethods(ServletServerHttpRequest request, + UriComponentsBuilder uriBuilder, + @PathVariable String repository) throws IOException { URI baseUri = uriBuilder.build().toUri(); - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); Links links = new Links(); - for ( Map.Entry entry : ((Map) repoMeta.queryMethods()) - .entrySet() ) { - String rel = repoMeta.rel() + "." + entry.getKey(); - URI path = buildUri( baseUri, repository, "search", entry.getKey() ); - RestResource resourceAnno = entry.getValue().method().getAnnotation( RestResource.class ); - if ( null != resourceAnno ) { - if ( StringUtils.hasText( resourceAnno.path() ) ) { - path = buildUri( baseUri, repository, "search", resourceAnno.path() ); - } - if ( StringUtils.hasText( resourceAnno.rel() ) ) { - rel = repoMeta.rel() + "." + resourceAnno.rel(); - } + for(Map.Entry entry : ((Map)repoMeta.queryMethods()) + .entrySet()) { + URI baseSearchUri = buildUri(baseUri, repository, "search"); + + // Check for customized rel and path + Method m = entry.getValue().method(); + if(m.isAnnotationPresent(RestResource.class)) { + RestResource resourceAnno = m.getAnnotation(RestResource.class); + links.add(new SimpleLink( + (StringUtils.hasText(resourceAnno.rel()) + ? repoMeta.rel() + "." + resourceAnno.rel() + : repoMeta.rel() + "." + entry.getKey()), + buildUri(baseSearchUri, + (StringUtils.hasText(resourceAnno.path()) + ? resourceAnno.path() + : entry.getKey())) + )); + } else { + // No customizations, use the default + links.add(new SimpleLink(repoMeta.rel() + "." + entry.getKey(), + buildUri(baseSearchUri, entry.getKey()))); } - links.add( new SimpleLink( rel, path ) ); } - return negotiateResponse( request, HttpStatus.OK, EMPTY_HEADERS, links ); + return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), links); } + /** + * Invoke a custom query method on a repository and page the results based on URL parameters supplied by the user or + * the default page size. + * + * @param request + * @param pageSort + * @param uriBuilder + * @param repository + * @param query + * + * @return + * + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/search/{query}", method = RequestMethod.GET ) @ResponseBody - public ResponseEntity query( ServletServerHttpRequest request, - PagingAndSorting pageSort, - UriComponentsBuilder uriBuilder, - @PathVariable String repository, - @PathVariable String query ) - throws InvocationTargetException, - IllegalAccessException, - IOException { + public ResponseEntity query(ServletServerHttpRequest request, + PagingAndSorting pageSort, + UriComponentsBuilder uriBuilder, + @PathVariable String repository, + @PathVariable String query) throws InvocationTargetException, + IllegalAccessException, + IOException { URI baseUri = uriBuilder.build().toUri(); - Page page = null; - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); Repository repo = repoMeta.repository(); - RepositoryQueryMethod queryMethod = repoMeta.queryMethod( query ); - if ( null == queryMethod ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); + RepositoryQueryMethod queryMethod = repoMeta.queryMethod(query); + if(null == queryMethod) { + return notFoundResponse(request); } Class[] paramTypes = queryMethod.paramTypes(); String[] paramNames = queryMethod.paramNames(); Object[] paramVals = new Object[paramTypes.length]; - for ( int i = 0; i < paramVals.length; i++ ) { - String queryVal = request.getServletRequest().getParameter( paramNames[i] ); - if ( String.class.isAssignableFrom( paramTypes[i] ) ) { + for(int i = 0; i < paramVals.length; i++) { + String queryVal = request.getServletRequest().getParameter(paramNames[i]); + if(null == queryVal) { + continue; + } + + RepositoryMetadata paramRepoMeta; + if(String.class.isAssignableFrom(paramTypes[i])) { // Param type is a String paramVals[i] = queryVal; - } else if ( Pageable.class.isAssignableFrom( paramTypes[i] ) ) { + } else if(Pageable.class.isAssignableFrom(paramTypes[i])) { // Handle paging paramVals[i] = pageSort; - } else if ( Sort.class.isAssignableFrom( paramTypes[i] ) ) { + } else if(Sort.class.isAssignableFrom(paramTypes[i])) { // Handle sorting paramVals[i] = (null != pageSort ? pageSort.getSort() : null); - } else if ( conversionService.canConvert( String.class, paramTypes[i] ) ) { + } else if(null != (paramRepoMeta = repositoryMetadataFor(paramTypes[i]))) { + // Complex parameter is a managed type + Serializable id = stringToSerializable(queryVal, + (Class)paramRepoMeta.entityMetadata() + .idAttribute() + .type()); + Object o = paramRepoMeta.repository().findOne(id); + if(null == o) { + return notFoundResponse(request); + } + + paramVals[i] = o; + } else if(conversionService.canConvert(String.class, paramTypes[i])) { // There's a converter from String -> param type - paramVals[i] = conversionService.convert( queryVal, paramTypes[i] ); + paramVals[i] = conversionService.convert(queryVal, paramTypes[i]); } else { // Param type isn't a "simple" type or no converter exists, try JSON try { - paramVals[i] = objectMapper.readValue( queryVal, paramTypes[i] ); - } catch ( IOException e ) { - throw new IllegalArgumentException( e ); + paramVals[i] = objectMapper.readValue(queryVal, paramTypes[i]); + } catch(IOException e) { + throw new IllegalArgumentException(e); } } } - Object result = queryMethod.method().invoke( repo, paramVals ); - Iterator iter; - if ( null != result ) { - if ( result instanceof Collection ) { - iter = ((Collection) result).iterator(); - } else if ( result instanceof Page ) { - page = (Page) result; - iter = page.iterator(); - } else { - List l = new ArrayList(); - l.add( result ); - iter = l.iterator(); - } - } else { - iter = Collections.emptyList().iterator(); + RepositoryMethodResponse response = new RepositoryMethodResponse(); + + Object result; + if(null == (result = queryMethod.method().invoke(repo, paramVals))) { + return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), response); } - Map resultMap = new HashMap(); - Links links = new Links(); - resultMap.put( LINKS, links.getLinks() ); - List resultList = new ArrayList(); - resultMap.put( "results", resultList ); + Iterator entities = Collections.emptyList().iterator(); + if(result instanceof Collection) { + entities = ((Collection)result).iterator(); + response.setTotalCount(((Collection)result).size()); + } else if(result instanceof Page) { + Page page = (Page)result; - boolean returnLinks = shouldReturnLinks( request.getServletRequest().getHeader( "Accept" ) ); - while ( iter.hasNext() ) { - Object obj = iter.next(); - RepositoryMetadata elemRepoMeta = repositoryMetadataFor( obj.getClass() ); - if ( null != elemRepoMeta ) { - String id = elemRepoMeta.entityMetadata().idAttribute().get( obj ).toString(); - if ( returnLinks ) { - String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName(); - URI path = buildUri( baseUri, repository, id ); - links.add( new SimpleLink( rel, path ) ); - } else { - Map entityDto = extractPropertiesLinkAware( repoMeta.rel(), - obj, - repoMeta.entityMetadata(), - buildUri( baseUri, repository, id ) ); - addSelfLink( baseUri, entityDto, repository, id ); - resultList.add( entityDto ); - } + if(page.hasContent()) { + entities = page.iterator(); } - } - // Add paging links - if ( null != page ) { - resultMap.put( "totalCount", page.getTotalElements() ); - resultMap.put( "totalPages", page.getTotalPages() ); - resultMap.put( "currentPage", page.getNumber() + 1 ); - // Copy over search parameters - UriComponentsBuilder urib = UriComponentsBuilder.fromUri( baseUri ).pathSegment( repository, "search", query ); - for ( String name : ((Map) request.getServletRequest().getParameterMap()).keySet() ) { - if ( !config.getPageParamName().equals( name ) && !config.getLimitParamName().equals( name ) - && !config.getSortParamName().equals( name ) ) { - urib.queryParam( name, request.getServletRequest().getParameter( name ) ); + // Set page counts in the response + response.setTotalCount(page.getTotalElements()); + response.setTotalPages(page.getTotalPages()); + response.setCurrentPage(page.getNumber() + 1); + + // Copy over parameters + UriComponentsBuilder selfUri = UriComponentsBuilder.fromUri(baseUri).pathSegment(repository, "search", query); + for(String name : request.getServletRequest().getParameterMap().keySet()) { + if(notPagingParam(name)) { + selfUri.queryParam(name, request.getServletRequest().getParameter(name)); } } - URI nextPrevBase = urib.build().toUri(); + // Add next/prev links as necessary + URI nextPrevBase = selfUri.build().toUri(); maybeAddPrevNextLink( nextPrevBase, repoMeta, @@ -484,7 +596,7 @@ public class RepositoryRestController !page.isFirstPage() && page.hasPreviousPage(), page.getNumber(), "prev", - links + response.getLinks() ); maybeAddPrevNextLink( nextPrevBase, @@ -494,110 +606,172 @@ public class RepositoryRestController !page.isLastPage() && page.hasNextPage(), page.getNumber() + 2, "next", - links + response.getLinks() ); + } else { - resultMap.put( "totalCount", resultList.size() ); + entities = Collections.singletonList(result).iterator(); } - return negotiateResponse( request, HttpStatus.OK, EMPTY_HEADERS, resultMap ); + while(entities.hasNext()) { + Object obj = entities.next(); + + RepositoryMetadata elemRepoMeta; + if(null == (elemRepoMeta = repositoryMetadataFor(obj.getClass()))) { + response.addResult(obj); + continue; + } + + // This object is managed by a repository + String id = elemRepoMeta.entityMetadata().idAttribute().get(obj).toString(); + if(shouldReturnLinks(request.getServletRequest().getHeader("Accept"))) { + String rel = elemRepoMeta.rel() + "." + elemRepoMeta.entityMetadata().type().getSimpleName(); + URI path = buildUri(baseUri, repository, id); + response.addLink(new SimpleLink(rel, path)); + } else { + Map entityDto = extractPropertiesLinkAware(repoMeta.rel(), + obj, + repoMeta.entityMetadata(), + buildUri(baseUri, repository, id)); + addSelfLink(baseUri, entityDto, repository, id); + response.addResult(entityDto); + } + } + + return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), response); } + /** + * Create a new entity by reading the incoming data and calling {@link CrudRepository#save(Object)} and letting the + * ID be auto-generated. + *

+ * To get the entity back in the body of the response, simpy add the URL parameter

returnBody=true
. + * + * @param request + * @param uriBuilder + * @param repository + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}", method = RequestMethod.POST ) @ResponseBody - public ResponseEntity create( ServletServerHttpRequest request, - HttpServletRequest servletRequest, - UriComponentsBuilder uriBuilder, - @PathVariable String repository ) - throws IOException { + public ResponseEntity create(ServletServerHttpRequest request, + UriComponentsBuilder uriBuilder, + @PathVariable String repository) throws IOException { URI baseUri = uriBuilder.build().toUri(); - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); CrudRepository repo = repoMeta.repository(); + MediaType incomingMediaType = request.getHeaders().getContentType(); - final Object incoming = readIncoming( request, incomingMediaType, repoMeta.entityMetadata().type() ); - if ( null == incoming ) { - return negotiateResponse( request, HttpStatus.BAD_REQUEST, EMPTY_HEADERS, null ); - } else { - if ( null != applicationContext ) { - applicationContext.publishEvent( new BeforeSaveEvent( incoming ) ); - } - Object savedEntity = repo.save( incoming ); - if ( null != applicationContext ) { - applicationContext.publishEvent( new AfterSaveEvent( savedEntity ) ); - } - String sId = repoMeta.entityMetadata().idAttribute().get( savedEntity ).toString(); - - URI selfUri = buildUri( baseUri, repository, sId ); - - HttpHeaders headers = new HttpHeaders(); - headers.set( LOCATION, selfUri.toString() ); - - Object body = null; - if ( null != servletRequest.getParameter( "returnBody" ) && "true".equals( servletRequest.getParameter( - "returnBody" ) ) ) { - Map entityDto = extractPropertiesLinkAware( repoMeta.rel(), - savedEntity, - repoMeta.entityMetadata(), - buildUri( baseUri, repository, sId ) ); - addSelfLink( baseUri, entityDto, repository, sId ); - body = entityDto; - } - return negotiateResponse( request, HttpStatus.CREATED, headers, body ); + Object incoming = readIncoming(request, incomingMediaType, repoMeta.entityMetadata().type()); + if(null == incoming) { + throw new HttpMessageNotReadableException("Could not create an instance of " + repoMeta.entityMetadata() + .type() + .getSimpleName() + " from input."); } + + publishEvent(new BeforeSaveEvent(incoming)); + Object savedEntity = repo.save(incoming); + publishEvent(new AfterSaveEvent(savedEntity)); + + String sId = repoMeta.entityMetadata().idAttribute().get(savedEntity).toString(); + URI selfUri = buildUri(baseUri, repository, sId); + + HttpHeaders headers = new HttpHeaders(); + headers.set(LOCATION, selfUri.toString()); + + Object body = null; + if(null != request.getServletRequest().getParameter("returnBody") + && "true".equals(request.getServletRequest().getParameter("returnBody"))) { + Map entityDto = extractPropertiesLinkAware(repoMeta.rel(), + savedEntity, + repoMeta.entityMetadata(), + buildUri(baseUri, repository, sId)); + addSelfLink(baseUri, entityDto, repository, sId); + body = entityDto; + } + + return negotiateResponse(request, HttpStatus.CREATED, headers, body); } + /** + * Retrieve a specific entity. + * + * @param request + * @param uriBuilder + * @param repository + * @param id + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/{id}", method = RequestMethod.GET ) @ResponseBody - public ResponseEntity entity( ServletServerHttpRequest request, - UriComponentsBuilder uriBuilder, - @PathVariable String repository, - @PathVariable String id ) - throws IOException { + public ResponseEntity entity(ServletServerHttpRequest request, + UriComponentsBuilder uriBuilder, + @PathVariable String repository, + @PathVariable String id) throws IOException { URI baseUri = uriBuilder.build().toUri(); - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); - Serializable serId = stringToSerializable( id, - (Class) repoMeta.entityMetadata() - .idAttribute() - .type() ); + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); + Serializable serId = stringToSerializable(id, + (Class)repoMeta.entityMetadata() + .idAttribute() + .type()); CrudRepository repo = repoMeta.repository(); - Object entity = repo.findOne( serId ); - if ( null == entity ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - HttpHeaders headers = new HttpHeaders(); - if ( null != repoMeta.entityMetadata().versionAttribute() ) { - Object version = repoMeta.entityMetadata().versionAttribute().get( entity ); - if ( null != version ) { - List etags = request.getHeaders().getIfNoneMatch(); - for ( String etag : etags ) { - if ( ("\"" + version.toString() + "\"").equals( etag ) ) { - return negotiateResponse( request, HttpStatus.NOT_MODIFIED, EMPTY_HEADERS, null ); - } - } - headers.set( "ETag", "\"" + version.toString() + "\"" ); - } - } - Map entityDto = extractPropertiesLinkAware( repoMeta.rel(), - entity, - repoMeta.entityMetadata(), - buildUri( baseUri, repository, id ) ); - addSelfLink( baseUri, entityDto, repository, id ); - - return negotiateResponse( request, HttpStatus.OK, headers, entityDto ); + Object entity = repo.findOne(serId); + if(null == entity) { + return notFoundResponse(request); } + HttpHeaders headers = new HttpHeaders(); + if(null != repoMeta.entityMetadata().versionAttribute()) { + Object version = repoMeta.entityMetadata().versionAttribute().get(entity); + if(null != version) { + List etags = request.getHeaders().getIfNoneMatch(); + for(String etag : etags) { + if(("\"" + version.toString() + "\"").equals(etag)) { + return negotiateResponse(request, HttpStatus.NOT_MODIFIED, new HttpHeaders(), null); + } + } + headers.set("ETag", "\"" + version.toString() + "\""); + } + } + Map entityDto = extractPropertiesLinkAware(repoMeta.rel(), + entity, + repoMeta.entityMetadata(), + buildUri(baseUri, repository, id)); + addSelfLink(baseUri, entityDto, repository, id); + + return negotiateResponse(request, HttpStatus.OK, headers, entityDto); } + /** + * Create an entity with a specific ID or update an existing entity. + * + * @param request + * @param uriBuilder + * @param repository + * @param id + * + * @return + * + * @throws IOException + * @throws IllegalAccessException + * @throws InstantiationException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/{id}", @@ -607,178 +781,209 @@ public class RepositoryRestController } ) @ResponseBody - public ResponseEntity createOrUpdate( ServletServerHttpRequest request, - UriComponentsBuilder uriBuilder, - @PathVariable String repository, - @PathVariable String id ) - throws IOException, - IllegalAccessException, - InstantiationException { + public ResponseEntity createOrUpdate(ServletServerHttpRequest request, + UriComponentsBuilder uriBuilder, + @PathVariable String repository, + @PathVariable String id) throws IOException, + IllegalAccessException, + InstantiationException { URI baseUri = uriBuilder.build().toUri(); - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); - Serializable serId = stringToSerializable( id, - (Class) repoMeta.entityMetadata() - .idAttribute() - .type() ); + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); + Serializable serId = stringToSerializable(id, + (Class)repoMeta.entityMetadata() + .idAttribute() + .type()); CrudRepository repo = repoMeta.repository(); Class domainType = repoMeta.entityMetadata().type(); - final MediaType incomingMediaType = request.getHeaders().getContentType(); - final Object incoming = readIncoming( request, incomingMediaType, domainType ); - if ( null == incoming ) { - throw new HttpMessageNotReadableException( "Could not create an instance of " + domainType.getSimpleName() + " from input." ); + boolean returnBody = true; + if(null != request.getServletRequest().getParameter("returnBody")) { + returnBody = Boolean.parseBoolean(request.getServletRequest().getParameter("returnBody")); + } + + MediaType incomingMediaType = request.getHeaders().getContentType(); + Object incoming; + if(null == (incoming = readIncoming(request, incomingMediaType, domainType))) { + throw new HttpMessageNotReadableException("Could not create an instance of " + domainType.getSimpleName() + " from input."); + } + + repoMeta.entityMetadata().idAttribute().set(serId, incoming); + if(request.getMethod() == HttpMethod.POST) { + + publishEvent(new BeforeSaveEvent(incoming)); + Object savedEntity = repo.save(incoming); + publishEvent(new AfterSaveEvent(savedEntity)); + + URI selfUri = buildUri(baseUri, repository, id); + + HttpHeaders headers = new HttpHeaders(); + headers.set(LOCATION, selfUri.toString()); + + return negotiateResponse(request, HttpStatus.CREATED, headers, (returnBody ? savedEntity : null)); } else { - repoMeta.entityMetadata().idAttribute().set( serId, incoming ); - if ( request.getMethod() == HttpMethod.POST ) { - if ( null != applicationContext ) { - applicationContext.publishEvent( new BeforeSaveEvent( incoming ) ); - } - Object savedEntity = repo.save( incoming ); - if ( null != applicationContext ) { - applicationContext.publishEvent( new AfterSaveEvent( savedEntity ) ); - } - URI selfUri = buildUri( baseUri, repository, id ); - HttpHeaders headers = new HttpHeaders(); - headers.set( LOCATION, selfUri.toString() ); - boolean returnBody = true; - if ( null != request.getServletRequest().getParameter( "returnBody" ) ) { - returnBody = Boolean.parseBoolean( request.getServletRequest().getParameter( "returnBody" ) ); - } - return negotiateResponse( request, HttpStatus.CREATED, headers, (returnBody ? savedEntity : null) ); - } else { - Object entity = repo.findOne( serId ); - if ( null == entity ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - for ( AttributeMetadata attrMeta : (Collection) repoMeta.entityMetadata() - .embeddedAttributes() - .values() ) { - Object incomingVal = attrMeta.get( incoming ); - if ( null != incomingVal ) { - attrMeta.set( incomingVal, entity ); - } - } - if ( null != applicationContext ) { - applicationContext.publishEvent( new BeforeSaveEvent( entity ) ); - } - Object savedEntity = repo.save( entity ); - if ( null != applicationContext ) { - applicationContext.publishEvent( new AfterSaveEvent( savedEntity ) ); - } - return negotiateResponse( request, HttpStatus.NO_CONTENT, EMPTY_HEADERS, null ); + Object entity; + if(null == (entity = repo.findOne(serId))) { + return notFoundResponse(request); + } + + for(AttributeMetadata attrMeta : (Collection)repoMeta.entityMetadata() + .embeddedAttributes() + .values()) { + Object incomingVal = attrMeta.get(incoming); + if(null != incomingVal) { + attrMeta.set(incomingVal, entity); } } + + publishEvent(new BeforeSaveEvent(entity)); + Object savedEntity = repo.save(entity); + publishEvent(new AfterSaveEvent(savedEntity)); + + return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), (returnBody ? savedEntity : null)); } } + /** + * Delete an entity. + * + * @param request + * @param repository + * @param id + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/{id}", method = RequestMethod.DELETE ) @ResponseBody - public ResponseEntity deleteEntity( ServletServerHttpRequest request, - @PathVariable String repository, - @PathVariable String id ) - throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); - Serializable serId = stringToSerializable( id, - (Class) repoMeta.entityMetadata() - .idAttribute() - .type() ); + public ResponseEntity deleteEntity(ServletServerHttpRequest request, + @PathVariable String repository, + @PathVariable String id) throws IOException { + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); + Serializable serId = stringToSerializable(id, + (Class)repoMeta.entityMetadata() + .idAttribute() + .type()); CrudRepository repo = repoMeta.repository(); - Object entity = repo.findOne( serId ); - if ( null == entity ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - if ( null != applicationContext ) { - applicationContext.publishEvent( new BeforeDeleteEvent( entity ) ); - } - repo.delete( serId ); - if ( null != applicationContext ) { - applicationContext.publishEvent( new AfterDeleteEvent( entity ) ); - } - return negotiateResponse( request, HttpStatus.NO_CONTENT, EMPTY_HEADERS, null ); + Object entity; + if(null == (entity = repo.findOne(serId))) { + return notFoundResponse(request); } + + publishEvent(new BeforeDeleteEvent(entity)); + repo.delete(serId); + publishEvent(new AfterDeleteEvent(entity)); + + return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null); } - + /** + * Retrieve the property of an entity. + * + * @param request + * @param uriBuilder + * @param repository + * @param id + * @param property + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/{id}/{property}", method = RequestMethod.GET ) @ResponseBody - public ResponseEntity propertyOfEntity( ServletServerHttpRequest request, - UriComponentsBuilder uriBuilder, - @PathVariable String repository, - @PathVariable String id, - @PathVariable String property ) - throws IOException { + public ResponseEntity propertyOfEntity(ServletServerHttpRequest request, + UriComponentsBuilder uriBuilder, + @PathVariable String repository, + @PathVariable String id, + @PathVariable String property) throws IOException { URI baseUri = uriBuilder.build().toUri(); - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); - Serializable serId = stringToSerializable( id, - (Class) repoMeta.entityMetadata() - .idAttribute() - .type() ); + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); + Serializable serId = stringToSerializable(id, + (Class)repoMeta.entityMetadata() + .idAttribute() + .type()); CrudRepository repo = repoMeta.repository(); - Object entity = repo.findOne( serId ); - if ( null == entity ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute( property ); - if ( null == attrMeta ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - Class attrType = attrMeta.elementType(); - if ( null == attrType ) { - attrType = attrMeta.type(); - } - RepositoryMetadata propRepoMeta = repositoryMetadataFor( attrType ); - Object propVal = attrMeta.get( entity ); - AttributeMetadata idAttr = propRepoMeta.entityMetadata().idAttribute(); - if ( null != propVal ) { - Links links = new Links(); - if ( propVal instanceof Collection ) { - for ( Object o : (Collection) propVal ) { - String propValId = idAttr.get( o ).toString(); - String rel = repository + "." - + entity.getClass().getSimpleName() + "." - + attrType.getSimpleName(); - URI path = buildUri( baseUri, repository, id, property, propValId ); - links.add( new SimpleLink( rel, path ) ); - } - } else if ( propVal instanceof Map ) { - for ( Map.Entry entry : ((Map) propVal).entrySet() ) { - String propValId = idAttr.get( entry.getValue() ).toString(); - URI path = buildUri( baseUri, repository, id, property, propValId ); - Object oKey = entry.getKey(); - String sKey; - if ( ClassUtils.isAssignable( oKey.getClass(), String.class ) ) { - sKey = (String) oKey; - } else { - sKey = conversionService.convert( oKey, String.class ); - } - String rel = repository + "." + entity.getClass().getSimpleName() + "." + sKey; - links.add( new SimpleLink( rel, path ) ); - } - } else { - String propValId = idAttr.get( propVal ).toString(); - String rel = repository + "." + entity.getClass().getSimpleName() + "." + property; - URI path = buildUri( baseUri, repository, id, property, propValId ); - links.add( new SimpleLink( rel, path ) ); - } - return negotiateResponse( request, HttpStatus.OK, EMPTY_HEADERS, links ); - } else { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } - } + Object entity; + if(null == (entity = repo.findOne(serId))) { + return notFoundResponse(request); } + + AttributeMetadata attrMeta; + if(null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { + return notFoundResponse(request); + } + + Class attrType; + if(null == (attrType = attrMeta.elementType())) { + attrType = attrMeta.type(); + } + + RepositoryMetadata propRepoMeta = repositoryMetadataFor(attrType); + + Object propVal; + if(null == (propVal = attrMeta.get(entity))) { + return notFoundResponse(request); + } + + AttributeMetadata idAttr = propRepoMeta.entityMetadata().idAttribute(); + Links links = new Links(); + if(propVal instanceof Collection) { + for(Object o : (Collection)propVal) { + String propValId = idAttr.get(o).toString(); + String rel = repository + "." + + entity.getClass().getSimpleName() + "." + + attrType.getSimpleName(); + URI path = buildUri(baseUri, repository, id, property, propValId); + links.add(new SimpleLink(rel, path)); + } + } else if(propVal instanceof Map) { + for(Map.Entry entry : ((Map)propVal).entrySet()) { + String propValId = idAttr.get(entry.getValue()).toString(); + URI path = buildUri(baseUri, repository, id, property, propValId); + Object oKey = entry.getKey(); + String sKey; + if(ClassUtils.isAssignable(oKey.getClass(), String.class)) { + sKey = (String)oKey; + } else { + sKey = conversionService.convert(oKey, String.class); + } + links.add(new SimpleLink(sKey, path)); + } + } else { + String propValId = idAttr.get(propVal).toString(); + String rel = repository + "." + entity.getClass().getSimpleName() + "." + property; + URI path = buildUri(baseUri, repository, id, property, propValId); + links.add(new SimpleLink(rel, path)); + } + + return negotiateResponse(request, HttpStatus.OK, new HttpHeaders(), links); } + /** + * Update the property of an entity if that property is also managed by a {@link CrudRepository}. + * + * @param request + * @param uriBuilder + * @param repository + * @param id + * @param property + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/{id}/{property}", @@ -788,121 +993,109 @@ public class RepositoryRestController } ) @ResponseBody - public ResponseEntity updatePropertyOfEntity( final ServletServerHttpRequest request, - UriComponentsBuilder uriBuilder, - @PathVariable String repository, - @PathVariable String id, - final @PathVariable String property ) throws IOException { + public ResponseEntity updatePropertyOfEntity(final ServletServerHttpRequest request, + UriComponentsBuilder uriBuilder, + @PathVariable String repository, + @PathVariable String id, + final @PathVariable String property) throws IOException { URI baseUri = uriBuilder.build().toUri(); - final RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); - Serializable serId = stringToSerializable( id, - (Class) repoMeta.entityMetadata() - .idAttribute() - .type() ); + final RepositoryMetadata repoMeta = repositoryMetadataFor(repository); + Serializable serId = stringToSerializable(id, + (Class)repoMeta.entityMetadata() + .idAttribute() + .type()); CrudRepository repo = repoMeta.repository(); - final Object entity = repo.findOne( serId ); - if ( null == entity ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - final AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute( property ); - if ( null == attrMeta ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - Object linked = attrMeta.get( entity ); - final AtomicReference rel = new AtomicReference(); - Handler> entityHandler = new Handler>() { - @Override public ResponseEntity handle( Object linkedEntity ) { - if ( attrMeta.isCollectionLike() ) { - Collection c = new ArrayList(); - Collection current = attrMeta.asCollection( entity ); - if ( request.getMethod() == HttpMethod.POST && null != current ) { - c.addAll( current ); - } - c.add( linkedEntity ); - attrMeta.set( c, entity ); - } else if ( attrMeta.isSetLike() ) { - Set s = new HashSet(); - Set current = attrMeta.asSet( entity ); - if ( request.getMethod() == HttpMethod.POST && null != current ) { - s.addAll( current ); - } - s.add( linkedEntity ); - attrMeta.set( s, entity ); - } else if ( attrMeta.isMapLike() ) { - Map m = new HashMap(); - Map current = attrMeta.asMap( entity ); - if ( request.getMethod() == HttpMethod.POST && null != current ) { - m.putAll( current ); - } - String key = rel.get(); - if ( null == key ) { - try { - return negotiateResponse( request, HttpStatus.NOT_ACCEPTABLE, EMPTY_HEADERS, null ); - } catch ( IOException e ) { - throw new RuntimeException( e ); - } - } else { - m.put( rel.get(), linkedEntity ); - attrMeta.set( m, entity ); - } - } else { - attrMeta.set( linkedEntity, entity ); - } - return null; + + final Object entity; + final AttributeMetadata attrMeta; + if(null == (entity = repo.findOne(serId)) || null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { + return notFoundResponse(request); + } + + Object linked = attrMeta.get(entity); + final AtomicReference rel = new AtomicReference(); + Handler> entityHandler = new Handler>() { + @Override public ResponseEntity handle(Object linkedEntity) { + + if(attrMeta.isCollectionLike()) { + Collection c = new ArrayList(); + Collection current = attrMeta.asCollection(entity); + if(request.getMethod() == HttpMethod.POST && null != current) { + c.addAll(current); } - }; - MediaType incomingMediaType = request.getHeaders().getContentType(); - if ( incomingMediaType.getSubtype().startsWith( "uri-list" ) ) { - BufferedReader in = new BufferedReader( new InputStreamReader( request.getBody() ) ); - String line; - while ( null != (line = in.readLine()) ) { - String sLinkUri = line.trim(); - Object o = resolveTopLevelResource( baseUri, sLinkUri ); - if ( null != o ) { - ResponseEntity possibleResponse = entityHandler.handle( o ); - if ( null != possibleResponse ) { - return possibleResponse; - } + c.add(linkedEntity); + attrMeta.set(c, entity); + } else if(attrMeta.isSetLike()) { + Set s = new HashSet(); + Set current = attrMeta.asSet(entity); + if(request.getMethod() == HttpMethod.POST && null != current) { + s.addAll(current); + } + s.add(linkedEntity); + attrMeta.set(s, entity); + } else if(attrMeta.isMapLike()) { + Map m = new HashMap(); + Map current = attrMeta.asMap(entity); + if(request.getMethod() == HttpMethod.POST && null != current) { + m.putAll(current); + } + String key = rel.get(); + if(null == key) { + try { + return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), null); + } catch(IOException e) { + throw new RuntimeException(e); } } + m.put(rel.get(), linkedEntity); + attrMeta.set(m, entity); } else { - final Map>> incoming = readIncoming( request, - incomingMediaType, - Map.class ); - for ( Map link : incoming.get( LINKS ) ) { - String sLinkUri = link.get( "href" ); - Object o = resolveTopLevelResource( baseUri, sLinkUri ); - rel.set( link.get( "rel" ) ); - if ( null != o ) { - ResponseEntity possibleResponse = entityHandler.handle( o ); - if ( null != possibleResponse ) { - return possibleResponse; - } - } - } + attrMeta.set(linkedEntity, entity); } - if ( null != applicationContext ) { - applicationContext.publishEvent( new BeforeSaveEvent( entity ) ); - applicationContext.publishEvent( new BeforeLinkSaveEvent( entity, linked ) ); - } - Object savedEntity = repo.save( entity ); - if ( null != applicationContext ) { - linked = attrMeta.get( savedEntity ); - applicationContext.publishEvent( new AfterLinkSaveEvent( savedEntity, linked ) ); - applicationContext.publishEvent( new AfterSaveEvent( savedEntity ) ); - } + return null; + } + }; - if ( request.getMethod() == HttpMethod.PUT ) { - return negotiateResponse( request, HttpStatus.NO_CONTENT, EMPTY_HEADERS, null ); - } else { - return negotiateResponse( request, HttpStatus.CREATED, EMPTY_HEADERS, null ); + MediaType incomingMediaType = request.getHeaders().getContentType(); + Links incomingLinks = readIncoming(request, incomingMediaType, Links.class); + for(Link l : incomingLinks.getLinks()) { + Object o; + if(null != (o = resolveTopLevelResource(baseUri, l.href().toString()))) { + ResponseEntity possibleResponse = entityHandler.handle(o); + if(null != possibleResponse) { + return possibleResponse; } } + + publishEvent(new BeforeSaveEvent(entity)); + publishEvent(new BeforeLinkSaveEvent(entity, linked)); + Object savedEntity = repo.save(entity); + linked = attrMeta.get(savedEntity); + publishEvent(new AfterLinkSaveEvent(savedEntity, linked)); + publishEvent(new AfterSaveEvent(savedEntity)); + } + + if(request.getMethod() == HttpMethod.PUT) { + return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null); + } else { + return negotiateResponse(request, HttpStatus.CREATED, new HttpHeaders(), null); } } + /** + * Clear all linked entities of a specific property. + * + * @param request + * @param repository + * @param id + * @param property + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/{id}/{property}", @@ -911,41 +1104,47 @@ public class RepositoryRestController } ) @ResponseBody - public ResponseEntity clearLinks( ServletServerHttpRequest request, - @PathVariable String repository, - @PathVariable String id, - @PathVariable String property ) - throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); - Serializable serId = stringToSerializable( id, - (Class) repoMeta.entityMetadata() - .idAttribute() - .type() ); + public ResponseEntity clearLinks(ServletServerHttpRequest request, + @PathVariable String repository, + @PathVariable String id, + @PathVariable String property) throws IOException { + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); CrudRepository repo = repoMeta.repository(); - final Object entity = repo.findOne( serId ); - if ( null == entity ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute( property ); - if ( null != attrMeta ) { - Object linked = attrMeta.get( entity ); - attrMeta.set( null, entity ); + Serializable serId = stringToSerializable(id, + (Class)repoMeta.entityMetadata() + .idAttribute() + .type()); - if ( null != applicationContext ) { - applicationContext.publishEvent( new BeforeLinkSaveEvent( entity, linked ) ); - } - Object savedEntity = repo.save( entity ); - if ( null != applicationContext ) { - applicationContext.publishEvent( new AfterLinkSaveEvent( savedEntity, null ) ); - } - - return negotiateResponse( request, HttpStatus.NO_CONTENT, EMPTY_HEADERS, null ); - } else { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } + Object entity; + AttributeMetadata attrMeta; + if(null == (entity = repo.findOne(serId)) || null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { + return notFoundResponse(request); } + + Object linked = attrMeta.get(entity); + attrMeta.set(null, entity); + + publishEvent(new BeforeLinkSaveEvent(entity, linked)); + Object savedEntity = repo.save(entity); + publishEvent(new AfterLinkSaveEvent(savedEntity, null)); + + return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null); } + /** + * Retrieve a linked entity from a parent entity. + * + * @param request + * @param uriBuilder + * @param repository + * @param id + * @param property + * @param linkedId + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/{id}/{property}/{linkedId}", @@ -954,54 +1153,64 @@ public class RepositoryRestController } ) @ResponseBody - public ResponseEntity linkedEntity( ServletServerHttpRequest request, - UriComponentsBuilder uriBuilder, - @PathVariable String repository, - @PathVariable String id, - @PathVariable String property, - @PathVariable String linkedId ) - throws IOException { + public ResponseEntity linkedEntity(ServletServerHttpRequest request, + UriComponentsBuilder uriBuilder, + @PathVariable String repository, + @PathVariable String id, + @PathVariable String property, + @PathVariable String linkedId) throws IOException { URI baseUri = uriBuilder.build().toUri(); - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); - Serializable serId = stringToSerializable( id, - (Class) repoMeta.entityMetadata() - .idAttribute() - .type() ); - CrudRepository repo = repoMeta.repository(); - final Object entity = repo.findOne( serId ); - if ( null != entity ) { - AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute( property ); - if ( null != attrMeta ) { - // Find linked entity - RepositoryMetadata linkedRepoMeta = repositoryMetadataFor( attrMeta ); - if ( null != linkedRepoMeta ) { - CrudRepository linkedRepo = linkedRepoMeta.repository(); - Serializable sChildId = stringToSerializable( linkedId, - (Class) linkedRepoMeta.entityMetadata() - .idAttribute() - .type() ); - Object linkedEntity = linkedRepo.findOne( sChildId ); - if ( null != linkedEntity ) { - Map entityDto = extractPropertiesLinkAware( linkedRepoMeta.rel(), - linkedEntity, - linkedRepoMeta.entityMetadata(), - buildUri( baseUri, - linkedRepoMeta.name(), - linkedId ) ); - URI selfUri = addSelfLink( baseUri, entityDto, linkedRepoMeta.name(), linkedId ); + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); - HttpHeaders headers = new HttpHeaders(); - headers.add( "Content-Location", selfUri.toString() ); - return negotiateResponse( request, HttpStatus.OK, headers, entityDto ); - } - } - } + AttributeMetadata attrMeta; + if(null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { + return notFoundResponse(request); } - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); + // Find linked entity + RepositoryMetadata linkedRepoMeta; + if(null == (linkedRepoMeta = repositoryMetadataFor(attrMeta))) { + return notFoundResponse(request); + } + + CrudRepository linkedRepo = linkedRepoMeta.repository(); + Serializable sChildId = stringToSerializable(linkedId, + (Class)linkedRepoMeta.entityMetadata() + .idAttribute() + .type()); + Object linkedEntity; + if(null == (linkedEntity = linkedRepo.findOne(sChildId))) { + return notFoundResponse(request); + } + + Map entityDto = extractPropertiesLinkAware(linkedRepoMeta.rel(), + linkedEntity, + linkedRepoMeta.entityMetadata(), + buildUri(baseUri, + linkedRepoMeta.name(), + linkedId)); + URI selfUri = addSelfLink(baseUri, entityDto, linkedRepoMeta.name(), linkedId); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Location", selfUri.toString()); + + return negotiateResponse(request, HttpStatus.OK, headers, entityDto); } + /** + * Delete a specific relationship between a child entity and its parent. + * + * @param request + * @param repository + * @param id + * @param property + * @param linkedId + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @RequestMapping( value = "/{repository}/{id}/{property}/{linkedId}", @@ -1010,127 +1219,184 @@ public class RepositoryRestController } ) @ResponseBody - public ResponseEntity deleteLink( ServletServerHttpRequest request, - @PathVariable String repository, - @PathVariable String id, - @PathVariable String property, - @PathVariable String linkedId ) throws IOException { - RepositoryMetadata repoMeta = repositoryMetadataFor( repository ); - Serializable serId = stringToSerializable( id, - (Class) repoMeta.entityMetadata() - .idAttribute() - .type() ); + public ResponseEntity deleteLink(ServletServerHttpRequest request, + @PathVariable String repository, + @PathVariable String id, + @PathVariable String property, + @PathVariable String linkedId) throws IOException { + RepositoryMetadata repoMeta = repositoryMetadataFor(repository); CrudRepository repo = repoMeta.repository(); - Object entity = repo.findOne( serId ); - if ( null == entity ) { - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); - } else { - AttributeMetadata attrMeta = repoMeta.entityMetadata().attribute( property ); - if ( null != attrMeta ) { - // Find linked entity - RepositoryMetadata linkedRepoMeta = repositoryMetadataFor( attrMeta ); - if ( null != linkedRepoMeta ) { - CrudRepository linkedRepo = linkedRepoMeta.repository(); - Serializable sChildId = stringToSerializable( linkedId, - (Class) linkedRepoMeta.entityMetadata() - .idAttribute() - .type() ); - Object linkedEntity = linkedRepo.findOne( sChildId ); - if ( null != linkedEntity ) { - // Remove linked entity from relationship based on property type - if ( attrMeta.isCollectionLike() ) { - Collection c = attrMeta.asCollection( entity ); - if ( null != c ) { - c.remove( linkedEntity ); - } - } else if ( attrMeta.isSetLike() ) { - Set s = attrMeta.asSet( entity ); - if ( null != s ) { - s.remove( linkedEntity ); - } - } else if ( attrMeta.isMapLike() ) { - Object keyToRemove = null; - Map m = attrMeta.asMap( entity ); - if ( null != m ) { - for ( Map.Entry entry : m.entrySet() ) { - Object val = entry.getValue(); - if ( null != val && val.equals( linkedEntity ) ) { - keyToRemove = entry.getKey(); - break; - } - } - if ( null != keyToRemove ) { - m.remove( keyToRemove ); - } - } - } else { - attrMeta.set( linkedEntity, entity ); - } + Serializable serId = stringToSerializable(id, + (Class)repoMeta.entityMetadata() + .idAttribute() + .type()); + Object entity; + AttributeMetadata attrMeta; + if(null == (entity = repo.findOne(serId)) || null == (attrMeta = repoMeta.entityMetadata().attribute(property))) { + return notFoundResponse(request); + } - return negotiateResponse( request, HttpStatus.NO_CONTENT, EMPTY_HEADERS, null ); + // Find linked entity + RepositoryMetadata linkedRepoMeta; + if(null == (linkedRepoMeta = repositoryMetadataFor(attrMeta))) { + return notFoundResponse(request); + } + + CrudRepository linkedRepo = linkedRepoMeta.repository(); + Serializable sChildId = stringToSerializable(linkedId, + (Class)linkedRepoMeta.entityMetadata() + .idAttribute() + .type()); + + Object linkedEntity; + if(null == (linkedEntity = linkedRepo.findOne(sChildId))) { + return notFoundResponse(request); + } + + // Remove linked entity from relationship based on property type + if(attrMeta.isCollectionLike()) { + Collection c = attrMeta.asCollection(entity); + if(null != c) { + c.remove(linkedEntity); + } + } else if(attrMeta.isSetLike()) { + Set s = attrMeta.asSet(entity); + if(null != s) { + s.remove(linkedEntity); + } + } else if(attrMeta.isMapLike()) { + Object keyToRemove = null; + Map m = attrMeta.asMap(entity); + if(null != m) { + for(Map.Entry entry : m.entrySet()) { + Object val = entry.getValue(); + if(null != val && val.equals(linkedEntity)) { + keyToRemove = entry.getKey(); + break; } } + if(null != keyToRemove) { + m.remove(keyToRemove); + } } + } else { + attrMeta.set(linkedEntity, entity); } - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); + return negotiateResponse(request, HttpStatus.NO_CONTENT, new HttpHeaders(), null); } + /** + * Send a 404 if no repository was found. + * + * @param e + * @param request + * + * @return + * + * @throws IOException + */ @ExceptionHandler(RepositoryNotFoundException.class) @ResponseBody - public ResponseEntity handleRepositoryNotFoundFailure( RepositoryNotFoundException e, - ServletServerHttpRequest request ) - throws IOException { - if ( LOG.isWarnEnabled() ) { - LOG.warn( "RepositoryNotFoundException: " + e.getMessage() ); + public ResponseEntity handleRepositoryNotFoundFailure(RepositoryNotFoundException e, + ServletServerHttpRequest request) throws IOException { + if(LOG.isWarnEnabled()) { + LOG.warn("RepositoryNotFoundException: " + e.getMessage()); } - return negotiateResponse( request, HttpStatus.NOT_FOUND, EMPTY_HEADERS, null ); + return notFoundResponse(request); } + @ExceptionHandler( + { + NullPointerException.class, + IllegalArgumentException.class, + IllegalStateException.class, + ClassCastException.class + } + ) + @ResponseBody + public ResponseEntity handleMiscFailures(Throwable t, + ServletServerHttpRequest request) throws IOException { + if(LOG.isErrorEnabled()) { + LOG.error(t.getMessage(), t); + } + return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), null); + } + + /** + * Send a 409 Conflict in case of concurrent modification. + * + * @param ex + * @param request + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @ExceptionHandler(OptimisticLockingFailureException.class) @ResponseBody - public ResponseEntity handleLockingFailure( OptimisticLockingFailureException ex, - ServletServerHttpRequest request ) - throws IOException { - LOG.error( ex.getMessage(), ex ); + public ResponseEntity handleLockingFailure(OptimisticLockingFailureException ex, + ServletServerHttpRequest request) throws IOException { + LOG.error(ex.getMessage(), ex); HttpHeaders headers = new HttpHeaders(); - headers.setContentType( MediaType.APPLICATION_JSON ); + headers.setContentType(MediaType.APPLICATION_JSON); Map m = new HashMap(); - m.put( "message", ex.getMessage() ); - return negotiateResponse( request, HttpStatus.CONFLICT, headers, objectMapper.writeValueAsBytes( m ) ); + m.put("message", ex.getMessage()); + return negotiateResponse(request, HttpStatus.CONFLICT, headers, objectMapper.writeValueAsBytes(m)); } + /** + * Send a 400 Bad Request in case of a validation failure. + * + * @param ex + * @param request + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) @ExceptionHandler(RepositoryConstraintViolationException.class) @ResponseBody - public ResponseEntity handleValidationFailure( RepositoryConstraintViolationException ex, - ServletServerHttpRequest request ) - throws IOException { - LOG.error( ex.getMessage(), ex ); + public ResponseEntity handleValidationFailure(RepositoryConstraintViolationException ex, + ServletServerHttpRequest request) throws IOException { + LOG.error(ex.getMessage(), ex); Map m = new HashMap(); List errors = new ArrayList(); - for ( FieldError fe : ex.getErrors().getFieldErrors() ) { - errors.add( fe.getDefaultMessage() ); + for(FieldError fe : ex.getErrors().getFieldErrors()) { + errors.add(fe.getDefaultMessage()); } - m.put( "errors", errors ); + m.put("errors", errors); - return negotiateResponse( request, HttpStatus.BAD_REQUEST, EMPTY_HEADERS, m ); + return negotiateResponse(request, HttpStatus.BAD_REQUEST, new HttpHeaders(), m); } + /** + * Send a 400 Bad Request in case no converter was found to process the input or output. + * + * @param ex + * @param request + * + * @return + * + * @throws IOException + */ @SuppressWarnings({"unchecked"}) - @ExceptionHandler(HttpMessageNotReadableException.class) + @ExceptionHandler({HttpMessageNotReadableException.class, HttpMessageNotWritableException.class}) @ResponseBody - public ResponseEntity handleMessageConversionFailure( HttpMessageNotReadableException ex, - ServletServerHttpRequest request ) - throws IOException { - LOG.error( ex.getMessage(), ex ); + public ResponseEntity handleMessageConversionFailure(HttpMessageNotReadableException ex, + ServletServerHttpRequest request) throws IOException { + LOG.error(ex.getMessage(), ex); + HttpHeaders headers = new HttpHeaders(); - headers.setContentType( MediaType.APPLICATION_JSON ); + headers.setContentType(MediaType.APPLICATION_JSON); Map m = new HashMap(); - m.put( "message", ex.getMessage() ); - return negotiateResponse( request, HttpStatus.BAD_REQUEST, headers, objectMapper.writeValueAsBytes( m ) ); + m.put("message", ex.getMessage()); + m.put("acceptableTypes", availableMediaTypes); + + return negotiateResponse(request, HttpStatus.BAD_REQUEST, headers, objectMapper.writeValueAsBytes(m)); } /* @@ -1138,131 +1404,137 @@ public class RepositoryRestController Internal helper methods ----------------------------------- */ - private static URI buildUri( URI baseUri, String... pathSegments ) { - return UriComponentsBuilder.fromUri( baseUri ).pathSegment( pathSegments ).build().toUri(); + private static URI buildUri(URI baseUri, String... pathSegments) { + return UriComponentsBuilder.fromUri(baseUri).pathSegment(pathSegments).build().toUri(); } @SuppressWarnings({"unchecked"}) - private URI addSelfLink( URI baseUri, Map model, String... pathComponents ) { - List links = (List) model.get( LINKS ); - if ( null == links ) { + private URI addSelfLink(URI baseUri, Map model, String... pathComponents) { + List links = (List)model.get(LINKS); + if(null == links) { links = new ArrayList(); - model.put( LINKS, links ); + model.put(LINKS, links); } - URI selfUri = buildUri( baseUri, pathComponents ); - links.add( new SimpleLink( SELF, selfUri ) ); + URI selfUri = buildUri(baseUri, pathComponents); + links.add(new SimpleLink(SELF, selfUri)); return selfUri; } @SuppressWarnings({"unchecked"}) - private void maybeAddPrevNextLink( URI resourceUri, - RepositoryMetadata repoMeta, - PagingAndSorting pageSort, - Page page, - boolean addIf, - int nextPage, - String rel, - Links links ) { - if ( null != page && addIf ) { - UriComponentsBuilder urib = UriComponentsBuilder.fromUri( resourceUri ); - urib.queryParam( config.getPageParamName(), nextPage ); // PageRequest is 0-based, so it's already (page - 1) - urib.queryParam( config.getLimitParamName(), page.getSize() ); - pageSort.addSortParameters( urib ); - links.add( new SimpleLink( repoMeta.rel() + "." + rel, urib.build().toUri() ) ); + private void maybeAddPrevNextLink(URI resourceUri, + RepositoryMetadata repoMeta, + PagingAndSorting pageSort, + Page page, + boolean addIf, + int nextPage, + String rel, + List links) { + if(null != page && addIf) { + UriComponentsBuilder urib = UriComponentsBuilder.fromUri(resourceUri); + urib.queryParam(config.getPageParamName(), nextPage); // PageRequest is 0-based, so it's already (page - 1) + urib.queryParam(config.getLimitParamName(), page.getSize()); + pageSort.addSortParameters(urib); + links.add(new SimpleLink(repoMeta.rel() + "." + rel, urib.build().toUri())); } } @SuppressWarnings({"unchecked"}) - private V stringToSerializable( String s, Class targetType ) { - if ( ClassUtils.isAssignable( targetType, String.class ) ) { - return (V) s; + private V stringToSerializable(String s, Class targetType) { + if(ClassUtils.isAssignable(targetType, String.class)) { + return (V)s; } else { - return conversionService.convert( s, targetType ); + return conversionService.convert(s, targetType); } } @SuppressWarnings({"unchecked"}) - private Object resolveTopLevelResource( URI baseUri, String uri ) { - URI href = URI.create( uri ); + private Object resolveTopLevelResource(URI baseUri, String uri) { + URI href = URI.create(uri); - URI relativeUri = baseUri.relativize( href ); - Stack uris = UriUtils.explode( baseUri, relativeUri ); + URI relativeUri = baseUri.relativize(href); + Stack uris = UriUtils.explode(baseUri, relativeUri); - if ( uris.size() > 1 ) { - String repoName = UriUtils.path( uris.get( 0 ) ); - String sId = UriUtils.path( uris.get( 1 ) ); + if(uris.size() > 1) { + String repoName = UriUtils.path(uris.get(0)); + String sId = UriUtils.path(uris.get(1)); - RepositoryMetadata repoMeta = repositoryMetadataFor( repoName ); - CrudRepository repo = repoMeta.repository(); - if ( null == repo ) { + RepositoryMetadata repoMeta = repositoryMetadataFor(repoName); + + CrudRepository repo; + if(null == (repo = repoMeta.repository())) { return null; } - EntityMetadata entityMeta = repoMeta.entityMetadata(); - if ( null == entityMeta ) { + + EntityMetadata entityMeta; + if(null == (entityMeta = repoMeta.entityMetadata())) { return null; } - Class idType = (Class) entityMeta.idAttribute().type(); - Serializable serId = stringToSerializable( sId, idType ); + Class idType = (Class)entityMeta.idAttribute().type(); + Serializable serId = stringToSerializable(sId, idType); - return repo.findOne( serId ); + return repo.findOne(serId); } return null; } @SuppressWarnings({"unchecked"}) - private V readIncoming( HttpInputMessage request, MediaType incomingMediaType, Class targetType ) throws IOException { - for ( HttpMessageConverter converter : httpMessageConverters ) { - if ( converter.canRead( targetType, incomingMediaType ) ) { - return (V) converter.read( targetType, request ); + private V readIncoming(HttpInputMessage request, MediaType incomingMediaType, Class targetType) + throws IOException { + // Check custom converters first + for(HttpMessageConverter conv : config.getCustomConverters()) { + if(conv.canRead(targetType, incomingMediaType)) { + return (V)conv.read(targetType, request); + } + } + // Use our always-available default list of converters + for(HttpMessageConverter conv : httpMessageConverters) { + if(conv.canRead(targetType, incomingMediaType)) { + return (V)conv.read(targetType, request); } } return null; } @SuppressWarnings({"unchecked"}) - private Map extractPropertiesLinkAware( String repoRel, - Object entity, - EntityMetadata entityMetadata, - URI baseUri ) { + private Map extractPropertiesLinkAware(String repoRel, + Object entity, + EntityMetadata entityMetadata, + URI baseUri) { final Map entityDto = new HashMap(); - for ( Map.Entry attrMeta : entityMetadata.embeddedAttributes().entrySet() ) { + for(Map.Entry attrMeta : entityMetadata.embeddedAttributes().entrySet()) { String name = attrMeta.getKey(); - Object val = attrMeta.getValue().get( entity ); - if ( null != val ) { - entityDto.put( name, val ); + Object val = attrMeta.getValue().get(entity); + if(null != val) { + entityDto.put(name, val); } } - for ( String attrName : entityMetadata.linkedAttributes().keySet() ) { - URI uri = buildUri( baseUri, attrName ); - Link l = new SimpleLink( repoRel + "." + entity.getClass().getSimpleName() + "." + attrName, uri ); - List links = (List) entityDto.get( LINKS ); - if ( null == links ) { + for(String attrName : entityMetadata.linkedAttributes().keySet()) { + URI uri = buildUri(baseUri, attrName); + Link l = new SimpleLink(repoRel + "." + entity.getClass().getSimpleName() + "." + attrName, uri); + List links = (List)entityDto.get(LINKS); + if(null == links) { links = new ArrayList(); - entityDto.put( LINKS, links ); + entityDto.put(LINKS, links); } - links.add( l ); + links.add(l); } return entityDto; } - private String viewName( String name ) { - return "org.springframework.data.rest." + name; - } - - private boolean shouldReturnLinks( String acceptHeader ) { - if ( null != acceptHeader ) { - List accept = MediaType.parseMediaTypes( acceptHeader ); - for ( MediaType mt : accept ) { - if ( mt.getSubtype().startsWith( "x-spring-data-verbose" ) ) { + private boolean shouldReturnLinks(String acceptHeader) { + if(null != acceptHeader) { + List accept = MediaType.parseMediaTypes(acceptHeader); + for(MediaType mt : accept) { + if(mt.getSubtype().startsWith("x-spring-data-verbose")) { return false; - } else if ( mt.getSubtype().startsWith( "x-spring-data-compact" ) ) { + } else if(mt.getSubtype().startsWith("x-spring-data-compact")) { return true; - } else if ( mt.getSubtype().equals( "uri-list" ) ) { + } else if(mt.getSubtype().equals("uri-list")) { return true; } } @@ -1270,109 +1542,111 @@ public class RepositoryRestController return false; } - private ResponseEntity noConverterFoundError( Class fromResponseType ) { - return new ResponseEntity( - String.format( "{\"message\": \"No converter found for class <%s>\"}", fromResponseType ), - HttpStatus.INTERNAL_SERVER_ERROR - ); + private void publishEvent(E event) { + if(null != applicationContext) { + applicationContext.publishEvent(event); + } + } + + private boolean notPagingParam(String name) { + return (!config.getPageParamName().equals(name) + && !config.getLimitParamName().equals(name) + && !config.getSortParamName().equals(name)); + } + + private ResponseEntity notFoundResponse(ServletServerHttpRequest request) throws IOException { + return negotiateResponse(request, HttpStatus.NOT_FOUND, new HttpHeaders(), null); } @SuppressWarnings({"unchecked"}) - private ResponseEntity negotiateResponse( final ServletServerHttpRequest request, - final HttpStatus status, - final HttpHeaders headers, - final Object resource ) throws IOException { + private ResponseEntity negotiateResponse(final ServletServerHttpRequest request, + final HttpStatus status, + final HttpHeaders headers, + final Object resource) throws IOException { - String jsonpParam = request.getServletRequest().getParameter( config.getJsonpParamName() ); + String jsonpParam = request.getServletRequest().getParameter(config.getJsonpParamName()); String jsonpOnErrParam = null; - if ( null != config.getJsonpOnErrParamName() ) { - jsonpOnErrParam = request.getServletRequest().getParameter( config.getJsonpOnErrParamName() ); + if(null != config.getJsonpOnErrParamName()) { + jsonpOnErrParam = request.getServletRequest().getParameter(config.getJsonpOnErrParamName()); } - HttpStatus responseStatus = status; - byte[] responseBody = null; - if ( null != resource ) { - List acceptableTypes = new ArrayList(); + if(null == resource) { + return maybeWrapJsonp(status, jsonpParam, jsonpOnErrParam, headers, null); + } - if ( !request.getHeaders().getAccept().isEmpty() && - !Arrays.equals( - request.getHeaders().getAccept().toArray(), - ALL_TYPES.toArray() - ) ) { - acceptableTypes.addAll( request.getHeaders().getAccept() ); - } else { - acceptableTypes.add( MediaType.APPLICATION_JSON ); - } - - for ( MediaType acceptType : acceptableTypes ) { - HttpMessageConverter converterToUse = null; - for ( HttpMessageConverter conv : config.getCustomConverters() ) { - if ( conv.canWrite( resource.getClass(), acceptType ) ) { - converterToUse = conv; - break; + MediaType acceptType = config.getDefaultMediaType(); + HttpMessageConverter converter = findWriteConverter(resource.getClass(), acceptType); + // If an Accept header is specified that isn't the catch-all, try and find a converter for it. + if(!MediaTypes.ACCEPT_ALL_TYPES.equals(request.getHeaders().getAccept())) { + for(MediaType mt : request.getHeaders().getAccept()) { + if(null != (converter = findWriteConverter(resource.getClass(), mt))) { + if(!"*".equals(mt.getSubtype())) { + acceptType = mt; } - } - if ( null == converterToUse ) { - for ( HttpMessageConverter conv : httpMessageConverters ) { - if ( conv.canWrite( resource.getClass(), acceptType ) ) { - converterToUse = conv; - break; - } - } - } - - if ( null != converterToUse ) { - final ByteArrayOutputStream bout = new ByteArrayOutputStream(); - converterToUse.write( resource, acceptType, new HttpOutputMessage() { - @Override public OutputStream getBody() throws IOException { - return bout; - } - - @Override public HttpHeaders getHeaders() { - return headers; - } - } ); - - if ( null != jsonpParam || null != jsonpOnErrParam ) { - headers.setContentType( JacksonUtil.APPLICATION_JAVASCRIPT ); - } - responseBody = bout.toByteArray(); - } else { - responseStatus = HttpStatus.NOT_ACCEPTABLE; - headers.setContentType( MediaType.TEXT_PLAIN ); - StringBuilder sb = new StringBuilder(); - if ( null != jsonpOnErrParam ) { - sb.append( "\"" ); - } - for ( MediaType mt : availableMediaTypes ) { - sb.append( mt.toString() ).append( '\n' ); - } - if ( null != jsonpOnErrParam ) { - sb.append( "\"" ); - } - responseBody = sb.toString().getBytes(); + break; } } } + headers.setContentType(acceptType); - if ( responseStatus.value() > 400 && (null != jsonpOnErrParam) ) { - String jsonp = jsonpOnErrParam + "(" + responseStatus.value() + "," + (null == responseBody ? "null" : new String( - responseBody )) + ")"; - responseBody = jsonp.getBytes(); - responseStatus = HttpStatus.OK; - } else if ( null != jsonpParam ) { - String jsonp = jsonpParam + "(" + (null == responseBody ? "null" : new String( responseBody )) + ")"; - responseBody = jsonp.getBytes(); + if(null == converter) { + throw new HttpMessageNotWritableException("No HttpMessageConverter found to handle " + resource.getClass()); } - if ( null == responseBody ) { - headers.setContentLength( 0 ); - } else { - headers.setContentLength( responseBody.length ); + final ByteArrayOutputStream bout = new ByteArrayOutputStream(); + converter.write(resource, headers.getContentType(), new HttpOutputMessage() { + @Override public OutputStream getBody() throws IOException { + return bout; + } + + @Override public HttpHeaders getHeaders() { + return headers; + } + }); + + return maybeWrapJsonp(status, jsonpParam, jsonpOnErrParam, headers, bout.toByteArray()); + } + + @SuppressWarnings({"unchecked"}) + private HttpMessageConverter findWriteConverter(Class type, MediaType mediaType) { + for(HttpMessageConverter conv : config.getCustomConverters()) { + if(conv.canWrite(type, mediaType)) { + return conv; + } } + for(HttpMessageConverter conv : httpMessageConverters) { + if(conv.canWrite(type, mediaType)) { + return conv; + } + } + return null; + } + private ResponseEntity maybeWrapJsonp(HttpStatus status, + String jsonpParam, + String jsonpOnErrParam, + HttpHeaders headers, + byte[] body) { - return new ResponseEntity( responseBody, headers, responseStatus ); + byte[] responseBody = (null == body ? new byte[0] : body); + if(status.value() >= 400 && null != jsonpOnErrParam) { + status = HttpStatus.OK; + responseBody = String.format("%s(%s, %s)", + jsonpOnErrParam, + status.value(), + (null != body ? new String(body) : null)) + .getBytes(); + headers.setContentType(MediaTypes.APPLICATION_JAVASCRIPT); + } else if(null != jsonpParam) { + responseBody = String.format("%s(%s)", + jsonpParam, + (null != body ? new String(body) : null)) + .getBytes(); + headers.setContentType(MediaTypes.APPLICATION_JAVASCRIPT); + } + headers.setContentLength(responseBody.length); + + return new ResponseEntity(responseBody, headers, status); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java index cdf80268c..109a18cfa 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerAdapter.java @@ -13,27 +13,27 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl */ public class RepositoryRestHandlerAdapter extends RequestMappingHandlerAdapter { - public RepositoryRestHandlerAdapter( RepositoryRestConfiguration config ) { - setCustomArgumentResolvers( Arrays.asList( + public RepositoryRestHandlerAdapter(RepositoryRestConfiguration config) { + setCustomArgumentResolvers(Arrays.asList( new ServerHttpRequestMethodArgumentResolver(), - new PagingAndSortingMethodArgumentResolver( config ) - ) ); + new PagingAndSortingMethodArgumentResolver(config) + )); // Add JSON converter for special Spring Data media type MappingJacksonHttpMessageConverter json = new MappingJacksonHttpMessageConverter(); json.setSupportedMediaTypes( - Arrays.asList( MediaType.APPLICATION_JSON, MediaType.valueOf( "application/x-spring-data+json" ) ) + Arrays.asList(MediaType.APPLICATION_JSON, MediaType.valueOf("application/x-spring-data+json")) ); - getMessageConverters().add( json ); + getMessageConverters().add(json); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } - @Override protected boolean supportsInternal( HandlerMethod handlerMethod ) { - return super.supportsInternal( handlerMethod ) - && RepositoryRestController.class.isAssignableFrom( handlerMethod.getBeanType() ); + @Override protected boolean supportsInternal(HandlerMethod handlerMethod) { + return super.supportsInternal(handlerMethod) + && RepositoryRestController.class.isAssignableFrom(handlerMethod.getBeanType()); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java index 003efd6d4..0288d8a57 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMapping.java @@ -23,7 +23,7 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping { private EntityManagerFactory entityManagerFactory; @Autowired(required = false) private List repositoryExporters = Collections.emptyList(); - private Set repositoryNames = new HashSet(); + private Set repositoryNames = new HashSet(); public RepositoryRestHandlerMapping() { setOrder(Ordered.HIGHEST_PRECEDENCE); @@ -31,18 +31,19 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping { @SuppressWarnings({"unchecked"}) @Override - protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { - if (repositoryNames.isEmpty() && !repositoryExporters.isEmpty()) { - for (RepositoryExporter re : repositoryExporters) { + protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) + throws Exception { + if(repositoryNames.isEmpty() && !repositoryExporters.isEmpty()) { + for(RepositoryExporter re : repositoryExporters) { repositoryNames.addAll(re.repositoryNames()); } } String[] parts = lookupPath.split("/"); - if (parts.length == 0) { + if(parts.length == 0) { // Root request return super.lookupHandlerMethod(lookupPath, request); } else { - if (repositoryNames.contains(parts[1])) { + if(repositoryNames.contains(parts[1])) { return super.lookupHandlerMethod(lookupPath, request); } else { return null; @@ -55,7 +56,7 @@ public class RepositoryRestHandlerMapping extends RequestMappingHandlerMapping { } @Override protected void extendInterceptors(List interceptors) { - if (null != entityManagerFactory) { + if(null != entityManagerFactory) { OpenEntityManagerInViewInterceptor omivi = new OpenEntityManagerInViewInterceptor(); omivi.setEntityManagerFactory(entityManagerFactory); interceptors.add(omivi); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java index 820572d53..b2a00d938 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/RepositoryRestMvcConfiguration.java @@ -29,7 +29,7 @@ public class RepositoryRestMvcConfiguration { } @Bean public JpaRepositoryExporter jpaRepositoryExporter() { - if ( null == customJpaRepositoryExporter ) { + if(null == customJpaRepositoryExporter) { return new JpaRepositoryExporter(); } else { return customJpaRepositoryExporter; @@ -37,19 +37,20 @@ public class RepositoryRestMvcConfiguration { } @Bean public ValidatingRepositoryEventListener validatingRepositoryEventListener() { - if ( null == validatingRepositoryEventListener ) { + if(null == validatingRepositoryEventListener) { return new ValidatingRepositoryEventListener(); } else { return validatingRepositoryEventListener; } } - @Bean public RepositoryRestController repositoryRestController() throws Exception { + @Bean public RepositoryRestController repositoryRestController() + throws Exception { return new RepositoryRestController(); } @Bean public RepositoryRestHandlerAdapter repositoryExporterHandlerAdapter() { - return new RepositoryRestHandlerAdapter( repositoryRestConfig ); + return new RepositoryRestHandlerAdapter(repositoryRestConfig); } @Bean public RepositoryRestHandlerMapping repositoryExporterHandlerMapping() { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ServerHttpRequestMethodArgumentResolver.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ServerHttpRequestMethodArgumentResolver.java index 172525f48..7413df49f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ServerHttpRequestMethodArgumentResolver.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/ServerHttpRequestMethodArgumentResolver.java @@ -15,16 +15,17 @@ import org.springframework.web.method.support.ModelAndViewContainer; */ public class ServerHttpRequestMethodArgumentResolver implements HandlerMethodArgumentResolver { - @Override public boolean supportsParameter( MethodParameter parameter ) { - return ClassUtils.isAssignable( parameter.getParameterType(), ServletServerHttpRequest.class ); + @Override public boolean supportsParameter(MethodParameter parameter) { + return ClassUtils.isAssignable(parameter.getParameterType(), ServletServerHttpRequest.class); } @Override - public Object resolveArgument( MethodParameter parameter, - ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - WebDataBinderFactory binderFactory ) throws Exception { - return new ServletServerHttpRequest( (HttpServletRequest) webRequest.getNativeRequest() ); + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) + throws Exception { + return new ServletServerHttpRequest((HttpServletRequest)webRequest.getNativeRequest()); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java index d041cb1f2..ca3d2426f 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/UriListHttpMessageConverter.java @@ -5,96 +5,101 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URI; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.springframework.data.rest.core.Link; +import org.springframework.data.rest.core.Links; import org.springframework.data.rest.core.SimpleLink; +import org.springframework.data.rest.repository.invoke.RepositoryMethodResponse; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.server.ServletServerHttpRequest; /** + * A special {@link org.springframework.http.converter.HttpMessageConverter} that can take various input formats and + * produce a plain-text list of URIs (or read the same). + * * @author Jon Brisbin */ public class UriListHttpMessageConverter extends AbstractHttpMessageConverter { - public static final Charset DEFAULT_CHARSET = Charset.forName( "ISO-8859-1" ); - public UriListHttpMessageConverter() { - super( new MediaType( "text", "uri-list", DEFAULT_CHARSET ) ); + super(MediaTypes.URI_LIST); } - @Override protected boolean supports( Class clazz ) { - return (List.class.isAssignableFrom( clazz ) - || Map.class.isAssignableFrom( clazz ) - || Links.class.isAssignableFrom( clazz )); + @Override protected boolean supports(Class clazz) { + return (RepositoryMethodResponse.class.isAssignableFrom(clazz) + || List.class.isAssignableFrom(clazz) + || Map.class.isAssignableFrom(clazz) + || Links.class.isAssignableFrom(clazz)); } @SuppressWarnings({"unchecked"}) @Override - protected Object readInternal( Class clazz, - HttpInputMessage inputMessage ) + protected Object readInternal(Class clazz, + HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { - String rel = inputMessage.getHeaders().getFirst( "x-spring-data-urilist-rel" ); - if ( null == rel ) { - rel = inputMessage.getHeaders().getLocation().getPath().substring( 1 ).replaceAll( "/", "." ); + String rel = inputMessage.getHeaders().getFirst("x-spring-data-urilist-rel"); + if(null == rel && inputMessage instanceof ServletServerHttpRequest) { + rel = ((ServletServerHttpRequest)inputMessage).getURI().getPath().substring(1).replaceAll("/", "."); } - BufferedReader reader = new BufferedReader( new InputStreamReader( inputMessage.getBody() ) ); - String line = null; - Object links = null; + BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody())); + String line; + Object links; try { links = clazz.newInstance(); - } catch ( InstantiationException e ) { - throw new HttpMessageNotReadableException( e.getMessage(), e ); - } catch ( IllegalAccessException e ) { - throw new HttpMessageNotReadableException( e.getMessage(), e ); + } catch(InstantiationException e) { + throw new HttpMessageNotReadableException(e.getMessage(), e); + } catch(IllegalAccessException e) { + throw new HttpMessageNotReadableException(e.getMessage(), e); } - while ( null != (line = reader.readLine()) ) { - if ( links instanceof Links ) { - ((Links) links).add( new SimpleLink( rel, URI.create( line.trim() ) ) ); - } else if ( links instanceof List ) { - ((List) links).add( new SimpleLink( rel, URI.create( line.trim() ) ) ); - } else if ( links instanceof Map ) { - List l = (List) ((Map) links).get( "_links" ); - if ( null == l ) { + while(null != (line = reader.readLine())) { + if(links instanceof Links) { + ((Links)links).add(new SimpleLink(rel, URI.create(line.trim()))); + } else if(links instanceof List) { + ((List)links).add(new SimpleLink(rel, URI.create(line.trim()))); + } else if(links instanceof Map) { + List l = (List)((Map)links).get("_links"); + if(null == l) { l = new ArrayList(); - ((Map) links).put( "_links", l ); + ((Map)links).put("_links", l); } - l.add( new SimpleLink( rel, URI.create( line.trim() ) ) ); + l.add(new SimpleLink(rel, URI.create(line.trim()))); } } return links; } @Override - protected void writeInternal( Object links, HttpOutputMessage outputMessage ) + protected void writeInternal(Object links, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { OutputStream body = outputMessage.getBody(); - if ( links instanceof Links ) { - for ( SimpleLink link : ((Links) links).getLinks() ) { - body.write( link.href().toASCIIString().getBytes() ); - body.write( '\n' ); + if(links instanceof Links) { + for(Link link : ((Links)links).getLinks()) { + body.write(link.href().toASCIIString().getBytes()); + body.write('\n'); } - } else if ( links instanceof List ) { - for ( Object o : (List) links ) { - if ( o instanceof Link ) { - body.write( ((Link) o).href().toASCIIString().getBytes() ); + } else if(links instanceof List) { + for(Object o : (List)links) { + if(o instanceof Link) { + body.write(((Link)o).href().toASCIIString().getBytes()); } else { - body.write( o.toString().getBytes() ); + body.write(o.toString().getBytes()); } - body.write( '\n' ); + body.write('\n'); } - } else if ( links instanceof Map ) { - writeInternal( ((Map) links).get( "_links" ), outputMessage ); + } else if(links instanceof Map) { + writeInternal(((Map)links).get("_links"), outputMessage); + } else if(links instanceof RepositoryMethodResponse) { + writeInternal(((RepositoryMethodResponse)links).getLinks(), outputMessage); } } diff --git a/spring-data-rest-webmvc/src/main/webapp/WEB-INF/web.xml b/spring-data-rest-webmvc/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 659c823d2..000000000 --- a/spring-data-rest-webmvc/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - exporter - org.springframework.data.rest.webmvc.RepositoryRestExporterServlet - 1 - - - - exporter - /* - - - diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/BaseSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/BaseSpec.groovy new file mode 100644 index 000000000..e5cd2bb74 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/BaseSpec.groovy @@ -0,0 +1,106 @@ +package org.springframework.data.rest.webmvc.spec + +import org.codehaus.jackson.map.ObjectMapper +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.ApplicationContext +import org.springframework.data.rest.test.webmvc.Address +import org.springframework.data.rest.test.webmvc.AddressRepository +import org.springframework.data.rest.test.webmvc.ApplicationConfig +import org.springframework.data.rest.test.webmvc.Person +import org.springframework.data.rest.test.webmvc.PersonRepository +import org.springframework.data.rest.test.webmvc.TestRepositoryEventListener +import org.springframework.data.rest.webmvc.RepositoryRestController +import org.springframework.data.rest.webmvc.RepositoryRestMvcConfiguration +import org.springframework.http.ResponseEntity +import org.springframework.http.server.ServletServerHttpRequest +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.orm.jpa.EntityManagerHolder +import org.springframework.test.context.ContextConfiguration +import org.springframework.web.util.UriComponentsBuilder +import spock.lang.Specification + +import javax.persistence.EntityManagerFactory + +import static org.springframework.transaction.support.TransactionSynchronizationManager.* + +/** + * @author Jon Brisbin + */ +@ContextConfiguration(classes = [ApplicationConfig, RepositoryRestMvcConfiguration]) +abstract class BaseSpec extends Specification { + + @Autowired ApplicationContext appCtx + @Autowired TestRepositoryEventListener listener + @Autowired RepositoryRestController controller + @Autowired EntityManagerFactory emf + @Autowired PersonRepository people + @Autowired AddressRepository addresses + UriComponentsBuilder baseUri + ObjectMapper mapper = new ObjectMapper() + + def setup() { + baseUri = UriComponentsBuilder.fromUriString("http://localhost:8080/data") + + if (!hasResource(emf)) { + bindResource(emf, new EntityManagerHolder(emf.createEntityManager())) + } + + for (Person p : people.findAll()) { + people.delete(p) + } + for (Address a : addresses.findAll()) { + addresses.delete(a) + } + } + + def readJson(ResponseEntity entity) { + mapper.readValue((byte[]) entity.body, Map) + } + + def createJsonRequest(method, path, query, obj) { + createRequest(method, path, null, "application/json", mapper.writeValueAsString(obj)) + } + + def createUriListRequest(method, path, query, obj) { + createRequest(method, path, null, "text/uri-list", obj.join("\n")) + } + + def createRequest(method, path, query) { + createRequest(method, path, null, null, null) + } + + def createRequest(method, path, query, contentType, content) { + def req = new MockHttpServletRequest( + serverPort: 8080, + requestURI: "/data/$path", + method: method + ) + if (query) { + req.queryString = URLEncoder.encode( + query.collect {k, v -> "$k=$v"}.join("&") + ) + } + if (contentType) { + req.contentType = contentType + } + if (content) { + req.content = content + } + + new ServletServerHttpRequest(req) + } + + def newPerson() { + people.save(new Person(name: "John Doe", addresses: [newAddress("Univille")])) + } + + def newAddress(city) { + addresses.save(new Address( + ["1234 W. 1st St."] as String[], + city, + "ST", + "12345" + )) + } + +} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy new file mode 100644 index 000000000..c08ded450 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/EventsSpec.groovy @@ -0,0 +1,47 @@ +package org.springframework.data.rest.webmvc.spec + +import org.springframework.data.rest.repository.RepositoryConstraintViolationException +import org.springframework.data.rest.test.webmvc.Person +import org.springframework.http.HttpStatus + +/** + * @author Jon Brisbin + */ +class EventsSpec extends BaseSpec { + + def "cannot save invalid entity"() { + + given: + def person = new Person() + def request = createJsonRequest("POST", "people", null, person) + + when: + controller.create(request, baseUri, "people") + + then: + thrown(RepositoryConstraintViolationException) + + } + + def "captures before and after events"() { + + given: + def person = new Person(name: "John Doe") + def request = createJsonRequest("POST", "people", ["returnBody": "true"], person) + def persId + listener.handlers << { evt, p -> + if (evt == "afterSave") + persId = "${p.id}" + } + + when: + def response = controller.create(request, baseUri, "people") + def returnedId = response.headers.getFirst('Location').tokenize("/").last() + + then: + response.statusCode == HttpStatus.CREATED + persId == returnedId + + } + +} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RelationshipsSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RelationshipsSpec.groovy new file mode 100644 index 000000000..4dcc898d2 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RelationshipsSpec.groovy @@ -0,0 +1,40 @@ +package org.springframework.data.rest.webmvc.spec + +import org.springframework.http.HttpStatus + +/** + * @author Jon Brisbin + */ +class RelationshipsSpec extends BaseSpec { + + def "saves entity relationship"() { + + given: + def person = newPerson() + def persId = person.id + def addr = newAddress("Smallville") + def addrId = addr.id + def request = createUriListRequest( + "POST", + "people/$persId/addresses", + null, + [baseUri.pathSegment("address", "$addrId").build().toUriString()] + ) + + when: + def response = controller.updatePropertyOfEntity(request, baseUri, "people", "$persId", "addresses") + + then: + response.statusCode == HttpStatus.CREATED + + when: + request = createRequest("GET", "people/$persId/addresses/$addrId", null) + response = controller.linkedEntity(request, baseUri, "people", "$persId", "addresses", "$addrId") + + then: + response.statusCode == HttpStatus.OK + readJson(response).city == "Smallville" + + } + +} diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy index f4d976cda..7c9131042 100644 --- a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/RepositoryRestControllerSpec.groovy @@ -21,6 +21,7 @@ import org.springframework.transaction.support.TransactionSynchronizationManager import org.springframework.ui.ExtendedModelMap import org.springframework.web.context.support.AnnotationConfigWebApplicationContext import org.springframework.web.util.UriComponentsBuilder +import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification @@ -29,6 +30,7 @@ import javax.persistence.EntityManagerFactory /** * @author Jon Brisbin */ +@Ignore class RepositoryRestControllerSpec extends Specification { @Shared diff --git a/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/TopLevelEntitySpec.groovy b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/TopLevelEntitySpec.groovy new file mode 100644 index 000000000..35088ac2d --- /dev/null +++ b/spring-data-rest-webmvc/src/test/groovy/org/springframework/data/rest/webmvc/spec/TopLevelEntitySpec.groovy @@ -0,0 +1,39 @@ +package org.springframework.data.rest.webmvc.spec + +import org.springframework.data.rest.test.webmvc.Person +import org.springframework.http.HttpStatus + +/** + * @author Jon Brisbin + */ +class TopLevelEntitySpec extends BaseSpec { + + def "saves top-level entity"() { + + given: + def person = new Person(name: "John Doe") + def request = createJsonRequest("POST", "people/1", null, person) + + when: + def response = controller.createOrUpdate(request, baseUri, "people", "1") + + then: + response.statusCode == HttpStatus.CREATED + + } + + def "retrieves top-level entity"() { + + given: + def person = newPerson() + def request = createRequest("GET", "people/${person.id}", null) + + when: + def response = controller.entity(request, baseUri, "people", "${person.id}") + + then: + response.statusCode == HttpStatus.OK + + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestBuilder.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestBuilder.java index 83a1e9b85..61d55f31d 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestBuilder.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/RestBuilder.java @@ -38,12 +38,12 @@ public class RestBuilder { private ConversionService conversionService = new DefaultConversionService(); private ClientHttpRequestFactory requestFactory; - private RestTemplate restTemplate; + private RestTemplate restTemplate; private HttpHeaders headers = new HttpHeaders(); private MediaType contentType; private Class responseType = byte[].class; - private Map uriParams; - private Object body; + private Map uriParams; + private Object body; private Closure errorHandler; public RestBuilder() { @@ -57,7 +57,7 @@ public class RestBuilder { public Object call(Closure cl) { RestBuilder b = null != requestFactory ? new RestBuilder(requestFactory) : new RestBuilder(); - if (null != errorHandler) { + if(null != errorHandler) { b.setErrorHandler(errorHandler); } b.conversionService = conversionService; @@ -78,7 +78,7 @@ public class RestBuilder { @SuppressWarnings({"unchecked"}) public Object post(String url) { - if (responseType == URI.class) { + if(responseType == URI.class) { return restTemplate.postForLocation(maybeAddParams(url), new HttpEntity(body, headers)); } else { return restTemplate.postForEntity(maybeAddParams(url), new HttpEntity(body, headers), responseType); @@ -87,7 +87,7 @@ public class RestBuilder { @SuppressWarnings({"unchecked"}) public Object put(String url) { - if (null != uriParams) { + if(null != uriParams) { restTemplate.put(maybeAddParams(url), new HttpEntity(body, headers), uriParams); } else { restTemplate.put(maybeAddParams(url), new HttpEntity(body, headers)); @@ -118,23 +118,24 @@ public class RestBuilder { @SuppressWarnings({"unchecked"}) public Object date(String date) { - for (String fmt : DATE_FORMATS) { + for(String fmt : DATE_FORMATS) { try { Date dte = new SimpleDateFormat(fmt).parse(date); headers.setDate(dte.getTime()); break; - } catch (ParseException e) {} + } catch(ParseException e) { + } } return this; } @SuppressWarnings({"unchecked"}) public Object header(String key, Object val) { - if (null != val) { - if (val instanceof List) { - headers.put(key, (List) val); - } else if (ClassUtils.isAssignable(val.getClass(), String.class)) { - headers.set(key, (String) val); + if(null != val) { + if(val instanceof List) { + headers.put(key, (List)val); + } else if(ClassUtils.isAssignable(val.getClass(), String.class)) { + headers.set(key, (String)val); } else { headers.set(key, conversionService.convert(val, String.class)); } @@ -156,7 +157,7 @@ public class RestBuilder { @SuppressWarnings({"unchecked"}) public Object param(String key, String value) { - if (null == uriParams) { + if(null == uriParams) { uriParams = new HashMap(); } uriParams.put(key, value); @@ -180,9 +181,10 @@ public class RestBuilder { public Object setErrorHandler(Closure errorHandler) { this.errorHandler = errorHandler; - if (null != errorHandler) { + if(null != errorHandler) { this.restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { - @Override public void handleError(ClientHttpResponse response) throws IOException { + @Override public void handleError(ClientHttpResponse response) + throws IOException { RestBuilder.this.errorHandler.call(response); } }); @@ -198,12 +200,12 @@ public class RestBuilder { @SuppressWarnings({"unchecked"}) private String maybeAddParams(String url) { StringBuffer buff = new StringBuffer(url); - if (null != uriParams) { + if(null != uriParams) { buff.append("?"); - for (Map.Entry entry : ((Map) uriParams).entrySet()) { + for(Map.Entry entry : ((Map)uriParams).entrySet()) { try { buff.append(entry.getKey()).append("=").append(URLEncoder.encode(entry.getValue(), "UTF-8")); - } catch (UnsupportedEncodingException e) { + } catch(UnsupportedEncodingException e) { throw new IllegalStateException(e); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Address.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Address.java index e271b1c95..ac7bfbbb2 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Address.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Address.java @@ -3,6 +3,7 @@ package org.springframework.data.rest.test.webmvc; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import javax.persistence.ManyToOne; /** * @author Jon Brisbin @@ -10,11 +11,13 @@ import javax.persistence.Id; @Entity public class Address { - @Id @GeneratedValue private Long id; - private String[] lines; - private String city; - private String province; - private String postalCode; + @Id @GeneratedValue private Long id; + private String[] lines; + private String city; + private String province; + private String postalCode; + @ManyToOne + private Person person; public Address() { } @@ -62,4 +65,12 @@ public class Address { this.postalCode = postalCode; } + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } + } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AddressRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AddressRepository.java index 7884a924b..8ee8d3169 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AddressRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/AddressRepository.java @@ -1,9 +1,13 @@ package org.springframework.data.rest.test.webmvc; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; /** * @author Jon Brisbin */ public interface AddressRepository extends CrudRepository { + + public Address findByPerson(@Param("person") Person person); + } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ApplicationConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ApplicationConfig.java new file mode 100644 index 000000000..a2909fa3d --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ApplicationConfig.java @@ -0,0 +1,64 @@ +package org.springframework.data.rest.test.webmvc; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.orm.jpa.JpaDialect; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaDialect; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Jon Brisbin + */ +@Configuration +@ComponentScan(basePackageClasses = ApplicationConfig.class) +@EnableJpaRepositories +@EnableTransactionManagement +public class ApplicationConfig { + + @Bean public DataSource dataSource() { + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } + + @Bean public EntityManagerFactory entityManagerFactory() { + HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); + vendorAdapter.setDatabase(Database.HSQL); + vendorAdapter.setGenerateDdl(true); + + LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); + factory.setJpaVendorAdapter(vendorAdapter); + factory.setPackagesToScan(getClass().getPackage().getName()); + factory.setDataSource(dataSource()); + + factory.afterPropertiesSet(); + + return factory.getObject(); + } + + @Bean public JpaDialect jpaDialect() { + return new HibernateJpaDialect(); + } + + @Bean public PlatformTransactionManager transactionManager() { + JpaTransactionManager txManager = new JpaTransactionManager(); + txManager.setEntityManagerFactory(entityManagerFactory()); + return txManager; + } + + @Bean public TestRepositoryEventListener testRepositoryEventListener() { + return new TestRepositoryEventListener(); + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Family.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Family.java index e5f3e7208..b1834137d 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Family.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Family.java @@ -12,10 +12,10 @@ import javax.persistence.OneToMany; @Entity public class Family { - @Id @GeneratedValue private Long id; - private String surname; + @Id @GeneratedValue private Long id; + private String surname; @OneToMany - private List members; + private List members; public Long getId() { return id; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/FamilyRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/FamilyRepository.java index aa4a5a174..96ebc1492 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/FamilyRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/FamilyRepository.java @@ -5,5 +5,6 @@ import org.springframework.data.repository.CrudRepository; /** * @author Jon Brisbin */ -public interface FamilyRepository extends CrudRepository { +public interface FamilyRepository + extends CrudRepository { } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Person.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Person.java index f6e14890c..f072d3427 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Person.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Person.java @@ -5,25 +5,25 @@ import java.util.Map; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import javax.persistence.MapKey; import javax.persistence.OneToMany; import javax.persistence.Version; -import org.springframework.data.rest.repository.annotation.RestResource; - /** * @author Jon Brisbin */ @Entity public class Person { - @Id @GeneratedValue private Long id; - private String name; + @Id @GeneratedValue private Long id; + private String name; @Version - private Long version; + private Long version; @OneToMany - private List
addresses; + private List
addresses; @OneToMany - private Map profiles; + @MapKey(name = "type") + private Map profiles; public Person() { } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonLoader.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonLoader.java index e8c67010b..31e7afb52 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonLoader.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonLoader.java @@ -9,9 +9,10 @@ import org.springframework.beans.factory.InitializingBean; /** * @author Jon Brisbin */ -public class PersonLoader implements InitializingBean { +public class PersonLoader + implements InitializingBean { - private PersonRepository personRepository; + private PersonRepository personRepository; private ProfileRepository profileRepository; private AddressRepository addressRepository; @@ -39,7 +40,8 @@ public class PersonLoader implements InitializingBean { this.addressRepository = addressRepository; } - @Override public void afterPropertiesSet() throws Exception { + @Override public void afterPropertiesSet() + throws Exception { Address pers1addr = addressRepository.save(new Address(new String[]{"1234 W. 1st St."}, "Univille", "ST", "12345")); Map pers1profiles = new HashMap(); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java index 3c7561c5e..77d8fb5f9 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonRepository.java @@ -4,7 +4,6 @@ import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.repository.annotation.RestResource; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java index 36dbc5ce8..c738f9872 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/PersonValidator.java @@ -10,7 +10,8 @@ import org.springframework.validation.Validator; /** * @author Jon Brisbin */ -public class PersonValidator implements Validator { +public class PersonValidator + implements Validator { private static final Logger LOG = LoggerFactory.getLogger(PersonValidator.class); @@ -19,7 +20,7 @@ public class PersonValidator implements Validator { } @Override public void validate(Object target, Errors errors) { - Person p = (Person) target; + Person p = (Person)target; LOG.debug("validating Person " + p); ValidationUtils.rejectIfEmpty(errors, "name", "field.name.required", "Field 'name' cannot be blank."); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Profile.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Profile.java index 1931603a9..a8e3957c7 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Profile.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/Profile.java @@ -3,6 +3,7 @@ package org.springframework.data.rest.test.webmvc; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import javax.persistence.ManyToOne; /** * @author Jon Brisbin @@ -10,9 +11,11 @@ import javax.persistence.Id; @Entity public class Profile { - @Id @GeneratedValue private Long id; - private String type; - private String url; + @Id @GeneratedValue private Long id; + private String type; + private String url; + @ManyToOne + private Person person; public Profile() { } @@ -38,29 +41,37 @@ public class Profile { this.url = url; } + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } + @Override public boolean equals(Object o) { - if (!(o instanceof Profile)) { + if(!(o instanceof Profile)) { return false; } - Profile p2 = (Profile) o; + Profile p2 = (Profile)o; boolean idEq; - if (null != id) { + if(null != id) { idEq = id.equals(p2.id); } else { idEq = p2.id == null; } boolean typeEq; - if (null != type) { + if(null != type) { typeEq = type.equals(p2.type); } else { typeEq = p2.type == null; } boolean urlEq; - if (null != url) { + if(null != url) { urlEq = url.equals(p2.url); } else { urlEq = p2.url == null; diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ProfileRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ProfileRepository.java index fc4ba7b3d..9dc8ccd55 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ProfileRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/ProfileRepository.java @@ -1,9 +1,13 @@ package org.springframework.data.rest.test.webmvc; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; /** * @author Jon Brisbin */ public interface ProfileRepository extends CrudRepository { + + public Address findByPerson(@Param("person") Person person); + } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/RestExporterWebInitializer.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/RestExporterWebInitializer.java new file mode 100644 index 000000000..9b3ee90c5 --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/RestExporterWebInitializer.java @@ -0,0 +1,33 @@ +package org.springframework.data.rest.test.webmvc; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; + +import org.springframework.data.rest.webmvc.RepositoryRestExporterServlet; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * @author Jon Brisbin + */ +public class RestExporterWebInitializer implements WebApplicationInitializer { + + @Override public void onStartup(ServletContext servletContext) throws ServletException { + // Create the 'root' Spring application context + AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext(); + rootContext.register(ApplicationConfig.class); + + // Manage the lifecycle of the root application context + servletContext.addListener(new ContextLoaderListener(rootContext)); + + // Register and map the dispatcher servlet + DispatcherServlet servlet = new RepositoryRestExporterServlet(); + ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", servlet); + dispatcher.setLoadOnStartup(1); + dispatcher.addMapping("/*"); + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/TestRepositoryEventListener.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/TestRepositoryEventListener.java new file mode 100644 index 000000000..8d00e4abe --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/TestRepositoryEventListener.java @@ -0,0 +1,32 @@ +package org.springframework.data.rest.test.webmvc; + +import java.util.ArrayList; +import java.util.List; + +import groovy.lang.Closure; +import org.springframework.data.rest.repository.context.AbstractRepositoryEventListener; + +/** + * @author Jon Brisbin + */ +public class TestRepositoryEventListener extends AbstractRepositoryEventListener { + + private List handlers = new ArrayList(); + + public List getHandlers() { + return handlers; + } + + @Override protected void onBeforeSave(Object entity) { + for(Closure cl : handlers) { + cl.call("beforeSave", entity); + } + } + + @Override protected void onAfterSave(Object entity) { + for(Closure cl : handlers) { + cl.call("afterSave", entity); + } + } + +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTestRepository.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTestRepository.java index 205c6214a..e3fe4fe1f 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTestRepository.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/test/webmvc/UuidTestRepository.java @@ -9,5 +9,6 @@ import org.springframework.data.rest.repository.annotation.RestResource; * @author Jon Brisbin */ @RestResource(exported = false) -public interface UuidTestRepository extends CrudRepository { +public interface UuidTestRepository + extends CrudRepository { } diff --git a/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml b/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml index 4edd1a862..910313d5b 100644 --- a/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml +++ b/spring-data-rest-webmvc/src/test/resources/META-INF/spring-data-rest/repositories-export.xml @@ -1,14 +1,10 @@ + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - - - + - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spring-data-rest-webmvc/src/test/resources/load_data.sh b/spring-data-rest-webmvc/src/test/resources/load_data.sh index 250e9386f..0c78835d7 100755 --- a/spring-data-rest-webmvc/src/test/resources/load_data.sh +++ b/spring-data-rest-webmvc/src/test/resources/load_data.sh @@ -6,6 +6,7 @@ curl -d 'http://localhost:8080/people/1 http://localhost:8080/people/2' -H "Content-Type: text/uri-list" http://localhost:8080/family/1/members curl -d '{"postalCode":"12345","province":"MO","lines":["1 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address curl -d "http://localhost:8080/address/1" -H "Content-Type: text/uri-list" http://localhost:8080/people/1/addresses +curl -d "http://localhost:8080/people/1" -X PUT -H "Content-Type: text/uri-list" http://localhost:8080/address/1/person curl -d '{"postalCode":"54321","province":"MO","lines":["2 W 1st St."],"city":"Univille"}' -H "Content-Type: application/json" http://localhost:8080/address curl -d "http://localhost:8080/address/2" -H "Content-Type: text/uri-list" http://localhost:8080/people/2/addresses curl -d '{"type" : "twitter", "url": "#!/johndoe"}' -H "Content-Type: application/json" http://localhost:8080/profile