1 /*
2  * Copyright 2016-2020 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      https://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.data.web;
17
18 import lombok.RequiredArgsConstructor;
19 import net.minidev.json.JSONArray;
20 import net.minidev.json.JSONObject;
21
22 import java.io.InputStream;
23 import java.lang.reflect.Method;
24 import java.lang.reflect.Type;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Map;
30
31 import org.aopalliance.intercept.MethodInterceptor;
32 import org.aopalliance.intercept.MethodInvocation;
33 import org.springframework.core.ResolvableType;
34 import org.springframework.core.annotation.AnnotationUtils;
35 import org.springframework.data.projection.Accessor;
36 import org.springframework.data.projection.MethodInterceptorFactory;
37 import org.springframework.data.util.ClassTypeInformation;
38 import org.springframework.data.util.TypeInformation;
39 import org.springframework.lang.Nullable;
40 import org.springframework.util.Assert;
41
42 import com.fasterxml.jackson.databind.ObjectMapper;
43 import com.jayway.jsonpath.Configuration;
44 import com.jayway.jsonpath.DocumentContext;
45 import com.jayway.jsonpath.JsonPath;
46 import com.jayway.jsonpath.Option;
47 import com.jayway.jsonpath.ParseContext;
48 import com.jayway.jsonpath.PathNotFoundException;
49 import com.jayway.jsonpath.TypeRef;
50 import com.jayway.jsonpath.spi.mapper.MappingProvider;
51
52 /**
53  * {@link MethodInterceptorFactory} to create a {@link MethodInterceptor} that will
54  *
55  * @author Oliver Gierke
56  * @soundtrack Jeff Coffin - Fruitcake (The Inside Of The Outside)
57  * @since 1.13
58  */

59 public class JsonProjectingMethodInterceptorFactory implements MethodInterceptorFactory {
60
61     private final ParseContext context;
62
63     /**
64      * Creates a new {@link JsonProjectingMethodInterceptorFactory} using the given {@link ObjectMapper}.
65      *
66      * @param mapper must not be {@literal null}.
67      */

68     public JsonProjectingMethodInterceptorFactory(MappingProvider mappingProvider) {
69
70         Assert.notNull(mappingProvider, "MappingProvider must not be null!");
71
72         Configuration build = Configuration.builder()//
73                 .options(Option.ALWAYS_RETURN_LIST)//
74                 .mappingProvider(mappingProvider)//
75                 .build();
76
77         this.context = JsonPath.using(build);
78     }
79
80     /*
81      * (non-Javadoc)
82      * @see org.springframework.data.projection.MethodInterceptorFactory#createMethodInterceptor(java.lang.Object, java.lang.Class)
83      */

84     @Override
85     public MethodInterceptor createMethodInterceptor(Object source, Class<?> targetType) {
86
87         DocumentContext context = InputStream.class.isInstance(source) ? this.context.parse((InputStream) source)
88                 : this.context.parse(source);
89
90         return new InputMessageProjecting(context);
91     }
92
93     /*
94      * (non-Javadoc)
95      * @see org.springframework.data.projection.MethodInterceptorFactory#supports(java.lang.Object, java.lang.Class)
96      */

97     @Override
98     public boolean supports(Object source, Class<?> targetType) {
99
100         if (InputStream.class.isInstance(source) || JSONObject.class.isInstance(source)
101                 || JSONArray.class.isInstance(source)) {
102             return true;
103         }
104
105         return Map.class.isInstance(source) && hasJsonPathAnnotation(targetType);
106     }
107
108     /**
109      * Returns whether the given type contains a method with a {@link org.springframework.data.web.JsonPath} annotation.
110      *
111      * @param type must not be {@literal null}.
112      * @return
113      */

114     private static boolean hasJsonPathAnnotation(Class<?> type) {
115
116         for (Method method : type.getMethods()) {
117             if (AnnotationUtils.findAnnotation(method, org.springframework.data.web.JsonPath.class) != null) {
118                 return true;
119             }
120         }
121
122         return false;
123     }
124
125     @RequiredArgsConstructor
126     private static class InputMessageProjecting implements MethodInterceptor {
127
128         private final DocumentContext context;
129
130         /*
131          * (non-Javadoc)
132          * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
133          */

134         @Nullable
135         @Override
136         public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
137
138             Method method = invocation.getMethod();
139             TypeInformation<Object> returnType = ClassTypeInformation.fromReturnTypeOf(method);
140             ResolvableType type = ResolvableType.forMethodReturnType(method);
141             boolean isCollectionResult = Collection.class.isAssignableFrom(type.getRawClass());
142             type = isCollectionResult ? type : ResolvableType.forClassWithGenerics(List.class, type);
143
144             Iterable<String> jsonPaths = getJsonPaths(method);
145
146             for (String jsonPath : jsonPaths) {
147
148                 try {
149
150                     if (returnType.getRequiredActualType().getType().isInterface()) {
151
152                         List<?> result = context.read(jsonPath);
153                         return result.isEmpty() ? null : result.get(0);
154                     }
155
156                     type = isCollectionResult && JsonPath.isPathDefinite(jsonPath)
157                             ? ResolvableType.forClassWithGenerics(List.class, type)
158                             : type;
159
160                     List<?> result = (List<?>) context.read(jsonPath, new ResolvableTypeRef(type));
161
162                     if (isCollectionResult && JsonPath.isPathDefinite(jsonPath)) {
163                         result = (List<?>) result.get(0);
164                     }
165
166                     return isCollectionResult ? result : result.isEmpty() ? null : result.get(0);
167
168                 } catch (PathNotFoundException o_O) {
169                     // continue with next path
170                 }
171             }
172
173             return null;
174         }
175
176         /**
177          * Returns the JSONPath expression to be used for the given method.
178          *
179          * @param method
180          * @return
181          */

182         private static Collection<String> getJsonPaths(Method method) {
183
184             org.springframework.data.web.JsonPath annotation = AnnotationUtils.findAnnotation(method,
185                     org.springframework.data.web.JsonPath.class);
186
187             if (annotation != null) {
188                 return Arrays.asList(annotation.value());
189             }
190
191             return Collections.singletonList("$.".concat(new Accessor(method).getPropertyName()));
192         }
193
194         @RequiredArgsConstructor
195         private static class ResolvableTypeRef extends TypeRef<Object> {
196
197             private final ResolvableType type;
198
199             /*
200              * (non-Javadoc)
201              * @see com.jayway.jsonpath.TypeRef#getType()
202              */

203             @Override
204             public Type getType() {
205                 return type.getType();
206             }
207         }
208     }
209 }
210