diff --git a/pom.xml b/pom.xml
index 3480731b3..86b80f697 100644
--- a/pom.xml
+++ b/pom.xml
@@ -206,6 +206,13 @@
test
+
+ com.jayway.jsonpath
+ json-path
+ ${jsonpath}
+ true
+
+
diff --git a/src/main/java/org/springframework/data/projection/Accessor.java b/src/main/java/org/springframework/data/projection/Accessor.java
new file mode 100644
index 000000000..dc0602562
--- /dev/null
+++ b/src/main/java/org/springframework/data/projection/Accessor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.projection;
+
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.util.Assert;
+
+/**
+ * Helper value to abstract an accessor.
+ *
+ * @author Oliver Gierke
+ * @soundtrack Benny Greb - Soulfood (Live)
+ * @since 1.13
+ */
+public final class Accessor {
+
+ private final PropertyDescriptor descriptor;
+ private final Method method;
+
+ /**
+ * Creates an {@link Accessor} for the given {@link Method}.
+ *
+ * @param method must not be {@literal null}.
+ * @throws IllegalArgumentException in case the given method is not an accessor method.
+ */
+ public Accessor(Method method) {
+
+ Assert.notNull(method, "Method must not be null!");
+
+ this.descriptor = BeanUtils.findPropertyForMethod(method);
+ this.method = method;
+
+ Assert.notNull(descriptor, String.format("Invoked method %s is no accessor method!", method));
+ }
+
+ /**
+ * Returns whether the accessor is a getter.
+ *
+ * @return
+ */
+ public boolean isGetter() {
+ return method.equals(descriptor.getReadMethod());
+ }
+
+ /**
+ * Returns whether the accessor is a setter.
+ *
+ * @return
+ */
+ public boolean isSetter() {
+ return method.equals(descriptor.getWriteMethod());
+ }
+
+ /**
+ * Returns the name of the property this accessor handles.
+ *
+ * @return will never be {@literal null}.
+ */
+ public String getPropertyName() {
+ return descriptor.getName();
+ }
+}
diff --git a/src/main/java/org/springframework/data/projection/MapAccessingMethodInterceptor.java b/src/main/java/org/springframework/data/projection/MapAccessingMethodInterceptor.java
index c02c22980..bf856c38a 100644
--- a/src/main/java/org/springframework/data/projection/MapAccessingMethodInterceptor.java
+++ b/src/main/java/org/springframework/data/projection/MapAccessingMethodInterceptor.java
@@ -15,14 +15,14 @@
*/
package org.springframework.data.projection;
-import java.beans.PropertyDescriptor;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
import java.lang.reflect.Method;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
-import org.springframework.beans.BeanUtils;
-import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
@@ -31,20 +31,10 @@ import org.springframework.util.ReflectionUtils;
* @author Oliver Gierke
* @since 1.10
*/
+@RequiredArgsConstructor
class MapAccessingMethodInterceptor implements MethodInterceptor {
- private final Map map;
-
- /**
- * Creates a new {@link MapAccessingMethodInterceptor} for the given {@link Map}.
- *
- * @param map must not be {@literal null}.
- */
- public MapAccessingMethodInterceptor(Map map) {
-
- Assert.notNull(map, "Map must not be null!");
- this.map = map;
- }
+ private final @NonNull Map map;
/*
* (non-Javadoc)
@@ -70,58 +60,4 @@ class MapAccessingMethodInterceptor implements MethodInterceptor {
throw new IllegalStateException("Should never get here!");
}
-
- /**
- * Helper value to abstract an accessor.
- *
- * @author Oliver Gierke
- */
- private static final class Accessor {
-
- private final PropertyDescriptor descriptor;
- private final Method method;
-
- /**
- * Creates an {@link Accessor} for the given {@link Method}.
- *
- * @param method must not be {@literal null}.
- * @throws IllegalArgumentException in case the given method is not an accessor method.
- */
- public Accessor(Method method) {
-
- Assert.notNull(method, "Method must not be null!");
-
- this.descriptor = BeanUtils.findPropertyForMethod(method);
- this.method = method;
-
- Assert.notNull(descriptor, String.format("Invoked method %s is no accessor method!", method));
- }
-
- /**
- * Returns whether the acessor is a getter.
- *
- * @return
- */
- public boolean isGetter() {
- return method.equals(descriptor.getReadMethod());
- }
-
- /**
- * Returns whether the accessor is a setter.
- *
- * @return
- */
- public boolean isSetter() {
- return method.equals(descriptor.getWriteMethod());
- }
-
- /**
- * Returns the name of the property this accessor handles.
- *
- * @return will never be {@literal null}.
- */
- public String getPropertyName() {
- return descriptor.getName();
- }
- }
}
diff --git a/src/main/java/org/springframework/data/projection/MethodInterceptorFactory.java b/src/main/java/org/springframework/data/projection/MethodInterceptorFactory.java
new file mode 100644
index 000000000..ec068ed4d
--- /dev/null
+++ b/src/main/java/org/springframework/data/projection/MethodInterceptorFactory.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.projection;
+
+import org.aopalliance.intercept.MethodInterceptor;
+
+/**
+ * SPI to create {@link MethodInterceptor} instances based on the given source object and the target type to produce. To
+ * be registered with a {@link ProxyProjectionFactory} to customize the way method executions on projection proxies are
+ * handled.
+ *
+ * @author Oliver Gierke
+ * @see ProxyProjectionFactory
+ * @soundtrack Henrik Freischlader Trio - Nobody Else To Blame (Openness)
+ * @since 1.13
+ */
+public interface MethodInterceptorFactory {
+
+ /**
+ * Returns the {@link MethodInterceptor} to be used for the given source object and target type.
+ *
+ * @param source will never be {@literal null}.
+ * @param targetType will never be {@literal null}.
+ * @return
+ */
+ MethodInterceptor createMethodInterceptor(Object source, Class> targetType);
+
+ /**
+ * Returns whether the current factory is supposed to be used to create a {@link MethodInterceptor} for proxy of the
+ * given target type.
+ *
+ * @param source will never be {@literal null}.
+ * @param targetType will never be {@literal null}.
+ * @return
+ */
+ boolean supports(Object source, Class> targetType);
+}
diff --git a/src/main/java/org/springframework/data/projection/ProjectionFactory.java b/src/main/java/org/springframework/data/projection/ProjectionFactory.java
index a4cfbb376..6e080632e 100644
--- a/src/main/java/org/springframework/data/projection/ProjectionFactory.java
+++ b/src/main/java/org/springframework/data/projection/ProjectionFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2015 the original author or authors.
+ * Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@ public interface ProjectionFactory {
T createProjection(Class projectionType, Object source);
/**
- * Creates a pojection instance for the given type.
+ * Creates a projection instance for the given type.
*
* @param projectionType the type to create, must not be {@literal null}.
* @return
diff --git a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java
index c2a653c83..94f07e004 100644
--- a/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java
+++ b/src/main/java/org/springframework/data/projection/ProxyProjectionFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2014-2105 the original author or authors.
+ * Copyright 2014-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,8 +47,19 @@ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware,
private static final boolean IS_JAVA_8 = org.springframework.util.ClassUtils.isPresent("java.util.Optional",
ProxyProjectionFactory.class.getClassLoader());
+ private final List factories;
private ClassLoader classLoader;
+ /**
+ * Creates a new {@link ProxyProjectionFactory}.
+ */
+ protected ProxyProjectionFactory() {
+
+ this.factories = new ArrayList();
+ this.factories.add(MapAccessingMethodInterceptorFactory.INSTANCE);
+ this.factories.add(PropertyAccessingMethodInvokerFactory.INSTANCE);
+ }
+
/**
* @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader)
* @deprecated rather set the {@link ClassLoader} directly via {@link #setBeanClassLoader(ClassLoader)}.
@@ -68,6 +79,20 @@ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware,
this.classLoader = classLoader;
}
+ /**
+ * Registers the given {@link MethodInterceptorFactory} to be used with the factory. Factories registered later enjoy
+ * precedence over previously registered ones.
+ *
+ * @param factory must not be {@literal null}.
+ * @since 1.13
+ */
+ public void registerMethodInvokerFactory(MethodInterceptorFactory factory) {
+
+ Assert.notNull(factory, "MethodInterceptorFactory must not be null!");
+
+ this.factories.add(0, factory);
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.rest.core.projection.ProjectionFactory#createProjection(java.lang.Object, java.lang.Class)
@@ -144,17 +169,33 @@ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware,
* @param projectionType must not be {@literal null}.
* @return
*/
- @SuppressWarnings("unchecked")
private MethodInterceptor getMethodInterceptor(Object source, Class> projectionType) {
- MethodInterceptor propertyInvocationInterceptor = source instanceof Map
- ? new MapAccessingMethodInterceptor((Map) source)
- : new PropertyAccessingMethodInterceptor(source);
+ MethodInterceptor propertyInvocationInterceptor = getFactoryFor(source, projectionType)
+ .createMethodInterceptor(source, projectionType);
return new ProjectingMethodInterceptor(this,
postProcessAccessorInterceptor(propertyInvocationInterceptor, source, projectionType));
}
+ /**
+ * Returns the {@link MethodInterceptorFactory} to be used with the given source object and target type.
+ *
+ * @param source must not be {@literal null}.
+ * @param projectionType must not be {@literal null}.
+ * @return
+ */
+ private MethodInterceptorFactory getFactoryFor(Object source, Class> projectionType) {
+
+ for (MethodInterceptorFactory factory : factories) {
+ if (factory.supports(source, projectionType)) {
+ return factory;
+ }
+ }
+
+ throw new IllegalStateException("No MethodInterceptorFactory found for type ".concat(source.getClass().getName()));
+ }
+
/**
* Post-process the given {@link MethodInterceptor} for the given source instance and projection type. Default
* implementation will simply return the given interceptor.
@@ -218,4 +259,61 @@ class ProxyProjectionFactory implements ProjectionFactory, ResourceLoaderAware,
return invocation.proceed();
}
}
+
+ /**
+ * {@link MethodInterceptorFactory} handling {@link Map}s as target objects.
+ *
+ * @author Oliver Gierke
+ */
+ private static enum MapAccessingMethodInterceptorFactory implements MethodInterceptorFactory {
+
+ INSTANCE;
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.projection.MethodInterceptorFactory#createMethodInterceptor(java.lang.Object, java.lang.Class)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public MethodInterceptor createMethodInterceptor(Object source, Class> targetType) {
+ return new MapAccessingMethodInterceptor((Map) source);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.projection.MethodInterceptorFactory#supports(java.lang.Object, java.lang.Class)
+ */
+ @Override
+ public boolean supports(Object source, Class> targetType) {
+ return Map.class.isInstance(source);
+ }
+ }
+
+ /**
+ * {@link MethodInterceptorFactory} to create a {@link PropertyAccessingMethodInterceptor} for arbitrary objects.
+ *
+ * @author Oliver Gierke
+ */
+ private static enum PropertyAccessingMethodInvokerFactory implements MethodInterceptorFactory {
+
+ INSTANCE;
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.projection.MethodInterceptorFactory#createMethodInterceptor(java.lang.Object, java.lang.Class)
+ */
+ @Override
+ public MethodInterceptor createMethodInterceptor(Object source, Class> targetType) {
+ return new PropertyAccessingMethodInterceptor(source);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.projection.MethodInterceptorFactory#supports(java.lang.Object, java.lang.Class)
+ */
+ @Override
+ public boolean supports(Object source, Class> targetType) {
+ return true;
+ }
+ }
}
diff --git a/src/main/java/org/springframework/data/web/JsonPath.java b/src/main/java/org/springframework/data/web/JsonPath.java
new file mode 100644
index 000000000..968b17840
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/JsonPath.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to explicitly declare a JSON Path expression on a projection interface.
+ *
+ * @author Oliver Gierke
+ * @soundtrack Andy McKee - RyLynn (Live book)
+ * @since 1.13
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
+public @interface JsonPath {
+
+ /**
+ * The JSON Path expression to be evaluated when the annotated method is invoked.
+ *
+ * @return
+ */
+ String value();
+}
diff --git a/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java b/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java
new file mode 100644
index 000000000..58a43328f
--- /dev/null
+++ b/src/main/java/org/springframework/data/web/JsonProjectingMethodInterceptorFactory.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2016 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.web;
+
+import lombok.RequiredArgsConstructor;
+import net.minidev.json.JSONArray;
+import net.minidev.json.JSONObject;
+
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Map;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.core.ResolvableType;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.data.projection.Accessor;
+import org.springframework.data.projection.MethodInterceptorFactory;
+import org.springframework.data.util.ClassTypeInformation;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.util.Assert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.DocumentContext;
+import com.jayway.jsonpath.JsonPath;
+import com.jayway.jsonpath.ParseContext;
+import com.jayway.jsonpath.TypeRef;
+import com.jayway.jsonpath.spi.mapper.MappingProvider;
+
+/**
+ * {@link MethodInterceptorFactory} to create a {@link MethodInterceptor} that will
+ *
+ * @author Oliver Gierke
+ * @soundtrack Jeff Coffin - Fruitcake (The Inside Of The Outside)
+ * @since 1.13
+ */
+public class JsonProjectingMethodInterceptorFactory implements MethodInterceptorFactory {
+
+ private final ParseContext context;
+
+ /**
+ * Creates a new {@link JsonProjectingMethodInterceptorFactory} using the given {@link ObjectMapper}.
+ *
+ * @param mapper must not be {@literal null}.
+ */
+ public JsonProjectingMethodInterceptorFactory(MappingProvider mappingProvider) {
+
+ Assert.notNull(mappingProvider, "MappingProvider must not be null!");
+
+ Configuration build = Configuration.builder()//
+ .mappingProvider(mappingProvider)//
+ .build();
+
+ this.context = JsonPath.using(build);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.projection.MethodInterceptorFactory#createMethodInterceptor(java.lang.Object, java.lang.Class)
+ */
+ @Override
+ public MethodInterceptor createMethodInterceptor(Object source, Class> targetType) {
+
+ DocumentContext context = InputStream.class.isInstance(source) ? this.context.parse((InputStream) source)
+ : this.context.parse(source);
+
+ return new InputMessageProjecting(context);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.projection.MethodInterceptorFactory#supports(java.lang.Object, java.lang.Class)
+ */
+ @Override
+ public boolean supports(Object source, Class> targetType) {
+
+ if (InputStream.class.isInstance(source) || JSONObject.class.isInstance(source)
+ || JSONArray.class.isInstance(source)) {
+ return true;
+ }
+
+ return Map.class.isInstance(source) && hasJsonPathAnnotation(targetType);
+ }
+
+ /**
+ * Returns whether the given type contains a method with a {@link org.springframework.data.web.JsonPath} annotation.
+ *
+ * @param type must not be {@literal null}.
+ * @return
+ */
+ private static boolean hasJsonPathAnnotation(Class> type) {
+
+ for (Method method : type.getMethods()) {
+ if (AnnotationUtils.findAnnotation(method, org.springframework.data.web.JsonPath.class) != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @RequiredArgsConstructor
+ private static class InputMessageProjecting implements MethodInterceptor {
+
+ private final DocumentContext context;
+
+ /*
+ * (non-Javadoc)
+ * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
+ */
+ @Override
+ public Object invoke(MethodInvocation invocation) throws Throwable {
+
+ Method method = invocation.getMethod();
+ TypeInformation