|
1 | 1 | package graphql.schema; |
2 | 2 |
|
3 | | -import graphql.GraphQLException; |
4 | 3 | import graphql.Internal; |
5 | 4 |
|
6 | | -import java.lang.reflect.Field; |
7 | | -import java.lang.reflect.InvocationTargetException; |
8 | | -import java.lang.reflect.Method; |
9 | | -import java.lang.reflect.Modifier; |
10 | | -import java.util.Arrays; |
11 | | -import java.util.Comparator; |
12 | | -import java.util.Map; |
13 | | -import java.util.Optional; |
14 | | -import java.util.concurrent.ConcurrentHashMap; |
15 | | -import java.util.concurrent.ConcurrentMap; |
16 | | -import java.util.concurrent.atomic.AtomicBoolean; |
17 | | -import java.util.function.Predicate; |
18 | | - |
19 | | -import static graphql.Assert.assertShouldNeverHappen; |
20 | | -import static graphql.Scalars.GraphQLBoolean; |
21 | | -import static graphql.schema.GraphQLTypeUtil.isNonNull; |
22 | | -import static graphql.schema.GraphQLTypeUtil.unwrapOne; |
23 | | - |
24 | 5 | /** |
25 | 6 | * This class is the guts of a property data fetcher and also used in AST code to turn |
26 | 7 | * in memory java objects into AST elements |
27 | 8 | */ |
28 | 9 | @Internal |
29 | 10 | public class PropertyDataFetcherHelper { |
30 | | - private static final AtomicBoolean USE_SET_ACCESSIBLE = new AtomicBoolean(true); |
31 | | - private static final AtomicBoolean USE_NEGATIVE_CACHE = new AtomicBoolean(true); |
32 | | - private static final ConcurrentMap<String, CachedMethod> METHOD_CACHE = new ConcurrentHashMap<>(); |
33 | | - private static final ConcurrentMap<String, Field> FIELD_CACHE = new ConcurrentHashMap<>(); |
34 | | - private static final ConcurrentMap<String, String> NEGATIVE_CACHE = new ConcurrentHashMap<>(); |
35 | 11 |
|
36 | | - private static class CachedMethod { |
37 | | - CachedMethod(Method method) { |
38 | | - this.method = method; |
39 | | - this.takesDataFetcherEnvironmentAsOnlyArgument = takesDataFetcherEnvironmentAsOnlyArgument(method); |
40 | | - } |
41 | | - |
42 | | - Method method; |
43 | | - boolean takesDataFetcherEnvironmentAsOnlyArgument; |
44 | | - } |
| 12 | + private static final PropertyFetchingImpl impl = new PropertyFetchingImpl(DataFetchingEnvironment.class); |
45 | 13 |
|
46 | 14 | public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType) { |
47 | | - return getPropertyValue(propertyName, object, graphQLType, null); |
| 15 | + return impl.getPropertyValue(propertyName, object, graphQLType, null); |
48 | 16 | } |
49 | 17 |
|
50 | 18 | public static Object getPropertyValue(String propertyName, Object object, GraphQLType graphQLType, DataFetchingEnvironment environment) { |
51 | | - if (object instanceof Map) { |
52 | | - return ((Map<?, ?>) object).get(propertyName); |
53 | | - } |
54 | | - |
55 | | - String cacheKey = mkKey(object, propertyName); |
56 | | - // lets try positive cache mechanisms first. If we have seen the method or field before |
57 | | - // then we invoke it directly without burning any cycles doing reflection. |
58 | | - CachedMethod cachedMethod = METHOD_CACHE.get(cacheKey); |
59 | | - if (cachedMethod != null) { |
60 | | - try { |
61 | | - return invokeMethod(object, environment, cachedMethod.method, cachedMethod.takesDataFetcherEnvironmentAsOnlyArgument); |
62 | | - } catch (NoSuchMethodException ignored) { |
63 | | - assertShouldNeverHappen("A method cached as '%s' is no longer available??", cacheKey); |
64 | | - } |
65 | | - } |
66 | | - Field cachedField = FIELD_CACHE.get(cacheKey); |
67 | | - if (cachedField != null) { |
68 | | - return invokeField(object, cachedField); |
69 | | - } |
70 | | - |
71 | | - // |
72 | | - // if we have tried all strategies before and they have all failed then we negatively cache |
73 | | - // the cacheKey and assume that its never going to turn up. This shortcuts the property lookup |
74 | | - // in systems where there was a `foo` graphql property but they never provided an POJO |
75 | | - // version of `foo`. |
76 | | - // |
77 | | - // we do this second because we believe in the positive cached version will mostly prevail |
78 | | - // but if we then look it up and negatively cache it then lest do that look up next |
79 | | - // |
80 | | - if (isNegativelyCached(cacheKey)) { |
81 | | - return null; |
82 | | - } |
83 | | - // |
84 | | - // ok we haven't cached it and we haven't negatively cached it so we have to find the POJO method which is the most |
85 | | - // expensive operation here |
86 | | - // |
87 | | - boolean dfeInUse = environment != null; |
88 | | - try { |
89 | | - MethodFinder methodFinder = (root, methodName) -> findPubliclyAccessibleMethod(cacheKey, propertyName, root, methodName, dfeInUse); |
90 | | - return getPropertyViaGetterMethod(object, propertyName, graphQLType, methodFinder, environment); |
91 | | - } catch (NoSuchMethodException ignored) { |
92 | | - try { |
93 | | - MethodFinder methodFinder = (aClass, methodName) -> findViaSetAccessible(cacheKey, propertyName, aClass, methodName, dfeInUse); |
94 | | - return getPropertyViaGetterMethod(object, propertyName, graphQLType, methodFinder, environment); |
95 | | - } catch (NoSuchMethodException ignored2) { |
96 | | - try { |
97 | | - return getPropertyViaFieldAccess(cacheKey, object, propertyName); |
98 | | - } catch (FastNoSuchMethodException e) { |
99 | | - // we have nothing to ask for and we have exhausted our lookup strategies |
100 | | - putInNegativeCache(cacheKey); |
101 | | - return null; |
102 | | - } |
103 | | - } |
104 | | - } |
105 | | - } |
106 | | - |
107 | | - private static boolean isNegativelyCached(String key) { |
108 | | - if (USE_NEGATIVE_CACHE.get()) { |
109 | | - return NEGATIVE_CACHE.containsKey(key); |
110 | | - } |
111 | | - return false; |
112 | | - } |
113 | | - |
114 | | - private static void putInNegativeCache(String key) { |
115 | | - if (USE_NEGATIVE_CACHE.get()) { |
116 | | - NEGATIVE_CACHE.put(key, key); |
117 | | - } |
118 | | - } |
119 | | - |
120 | | - private interface MethodFinder { |
121 | | - Method apply(Class<?> aClass, String s) throws NoSuchMethodException; |
122 | | - } |
123 | | - |
124 | | - private static Object getPropertyViaGetterMethod(Object object, String propertyName, GraphQLType graphQLType, MethodFinder methodFinder, DataFetchingEnvironment environment) throws NoSuchMethodException { |
125 | | - if (isBooleanProperty(graphQLType)) { |
126 | | - try { |
127 | | - return getPropertyViaGetterUsingPrefix(object, propertyName, "is", methodFinder, environment); |
128 | | - } catch (NoSuchMethodException e) { |
129 | | - return getPropertyViaGetterUsingPrefix(object, propertyName, "get", methodFinder, environment); |
130 | | - } |
131 | | - } else { |
132 | | - return getPropertyViaGetterUsingPrefix(object, propertyName, "get", methodFinder, environment); |
133 | | - } |
134 | | - } |
135 | | - |
136 | | - private static Object getPropertyViaGetterUsingPrefix(Object object, String propertyName, String prefix, MethodFinder methodFinder, DataFetchingEnvironment environment) throws NoSuchMethodException { |
137 | | - String getterName = prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); |
138 | | - Method method = methodFinder.apply(object.getClass(), getterName); |
139 | | - return invokeMethod(object, environment, method, takesDataFetcherEnvironmentAsOnlyArgument(method)); |
140 | | - } |
141 | | - |
142 | | - /** |
143 | | - * Invoking public methods on package-protected classes via reflection |
144 | | - * causes exceptions. This method searches a class's hierarchy for |
145 | | - * public visibility parent classes with the desired getter. This |
146 | | - * particular case is required to support AutoValue style data classes, |
147 | | - * which have abstract public interfaces implemented by package-protected |
148 | | - * (generated) subclasses. |
149 | | - */ |
150 | | - private static Method findPubliclyAccessibleMethod(String cacheKey, String propertyName, Class<?> rootClass, String methodName, boolean dfeInUse) throws NoSuchMethodException { |
151 | | - Class<?> currentClass = rootClass; |
152 | | - while (currentClass != null) { |
153 | | - if (Modifier.isPublic(currentClass.getModifiers())) { |
154 | | - if (dfeInUse) { |
155 | | - // |
156 | | - // try a getter that takes DataFetchingEnvironment first (if we have one) |
157 | | - try { |
158 | | - Method method = currentClass.getMethod(methodName, DataFetchingEnvironment.class); |
159 | | - if (Modifier.isPublic(method.getModifiers())) { |
160 | | - METHOD_CACHE.putIfAbsent(cacheKey, new CachedMethod(method)); |
161 | | - return method; |
162 | | - } |
163 | | - } catch (NoSuchMethodException e) { |
164 | | - // ok try the next approach |
165 | | - } |
166 | | - } |
167 | | - Method method = currentClass.getMethod(methodName); |
168 | | - if (Modifier.isPublic(method.getModifiers())) { |
169 | | - METHOD_CACHE.putIfAbsent(cacheKey, new CachedMethod(method)); |
170 | | - return method; |
171 | | - } |
172 | | - } |
173 | | - currentClass = currentClass.getSuperclass(); |
174 | | - } |
175 | | - assert rootClass != null; |
176 | | - return rootClass.getMethod(methodName); |
177 | | - } |
178 | | - |
179 | | - private static Method findViaSetAccessible(String cacheKey, String propertyName, Class<?> aClass, String methodName, boolean dfeInUse) throws NoSuchMethodException { |
180 | | - if (!USE_SET_ACCESSIBLE.get()) { |
181 | | - throw new FastNoSuchMethodException(methodName); |
182 | | - } |
183 | | - Class<?> currentClass = aClass; |
184 | | - while (currentClass != null) { |
185 | | - Predicate<Method> whichMethods = mth -> { |
186 | | - if (dfeInUse) { |
187 | | - return hasZeroArgs(mth) || takesDataFetcherEnvironmentAsOnlyArgument(mth); |
188 | | - } |
189 | | - return hasZeroArgs(mth); |
190 | | - }; |
191 | | - Method[] declaredMethods = currentClass.getDeclaredMethods(); |
192 | | - Optional<Method> m = Arrays.stream(declaredMethods) |
193 | | - .filter(mth -> methodName.equals(mth.getName())) |
194 | | - .filter(whichMethods) |
195 | | - .min(mostMethodArgsFirst()); |
196 | | - if (m.isPresent()) { |
197 | | - try { |
198 | | - // few JVMs actually enforce this but it might happen |
199 | | - Method method = m.get(); |
200 | | - method.setAccessible(true); |
201 | | - METHOD_CACHE.putIfAbsent(cacheKey, new CachedMethod(method)); |
202 | | - return method; |
203 | | - } catch (SecurityException ignored) { |
204 | | - } |
205 | | - } |
206 | | - currentClass = currentClass.getSuperclass(); |
207 | | - } |
208 | | - throw new FastNoSuchMethodException(methodName); |
209 | | - } |
210 | | - |
211 | | - private static Object getPropertyViaFieldAccess(String cacheKey, Object object, String propertyName) throws FastNoSuchMethodException { |
212 | | - Class<?> aClass = object.getClass(); |
213 | | - try { |
214 | | - Field field = aClass.getField(propertyName); |
215 | | - FIELD_CACHE.putIfAbsent(cacheKey, field); |
216 | | - return field.get(object); |
217 | | - } catch (NoSuchFieldException e) { |
218 | | - if (!USE_SET_ACCESSIBLE.get()) { |
219 | | - throw new FastNoSuchMethodException(cacheKey); |
220 | | - } |
221 | | - // if not public fields then try via setAccessible |
222 | | - try { |
223 | | - Field field = aClass.getDeclaredField(propertyName); |
224 | | - field.setAccessible(true); |
225 | | - FIELD_CACHE.putIfAbsent(cacheKey, field); |
226 | | - return field.get(object); |
227 | | - } catch (SecurityException | NoSuchFieldException ignored2) { |
228 | | - throw new FastNoSuchMethodException(cacheKey); |
229 | | - } catch (IllegalAccessException e1) { |
230 | | - throw new GraphQLException(e); |
231 | | - } |
232 | | - } catch (IllegalAccessException e) { |
233 | | - throw new GraphQLException(e); |
234 | | - } |
235 | | - } |
236 | | - |
237 | | - private static Object invokeMethod(Object object, DataFetchingEnvironment environment, Method method, boolean takesDataFetcherEnvironmentAsOnlyArgument) throws FastNoSuchMethodException { |
238 | | - try { |
239 | | - if (takesDataFetcherEnvironmentAsOnlyArgument) { |
240 | | - if (environment == null) { |
241 | | - throw new FastNoSuchMethodException(method.getName()); |
242 | | - } |
243 | | - return method.invoke(object, environment); |
244 | | - } else { |
245 | | - return method.invoke(object); |
246 | | - } |
247 | | - } catch (IllegalAccessException | InvocationTargetException e) { |
248 | | - throw new GraphQLException(e); |
249 | | - } |
250 | | - } |
251 | | - |
252 | | - private static Object invokeField(Object object, Field field) { |
253 | | - try { |
254 | | - return field.get(object); |
255 | | - } catch (IllegalAccessException e) { |
256 | | - throw new GraphQLException(e); |
257 | | - } |
258 | | - } |
259 | | - |
260 | | - @SuppressWarnings("SimplifiableIfStatement") |
261 | | - private static boolean isBooleanProperty(GraphQLType graphQLType) { |
262 | | - if (graphQLType == GraphQLBoolean) { |
263 | | - return true; |
264 | | - } |
265 | | - if (isNonNull(graphQLType)) { |
266 | | - return unwrapOne(graphQLType) == GraphQLBoolean; |
267 | | - } |
268 | | - return false; |
| 19 | + return impl.getPropertyValue(propertyName, object, graphQLType, environment); |
269 | 20 | } |
270 | 21 |
|
271 | 22 | public static void clearReflectionCache() { |
272 | | - METHOD_CACHE.clear(); |
273 | | - FIELD_CACHE.clear(); |
274 | | - NEGATIVE_CACHE.clear(); |
| 23 | + impl.clearReflectionCache(); |
275 | 24 | } |
276 | 25 |
|
277 | 26 | public static boolean setUseSetAccessible(boolean flag) { |
278 | | - return USE_SET_ACCESSIBLE.getAndSet(flag); |
| 27 | + return impl.setUseSetAccessible(flag); |
279 | 28 | } |
280 | 29 |
|
281 | 30 | public static boolean setUseNegativeCache(boolean flag) { |
282 | | - return USE_NEGATIVE_CACHE.getAndSet(flag); |
283 | | - } |
284 | | - |
285 | | - private static String mkKey(Object object, String propertyName) { |
286 | | - Class<?> clazz = object.getClass(); |
287 | | - ClassLoader classLoader = clazz.getClassLoader(); |
288 | | - if (classLoader != null) { |
289 | | - return classLoader.hashCode() + "__" + clazz.getName() + "__" + propertyName; |
290 | | - } else { |
291 | | - return clazz.getName() + "__" + propertyName; |
292 | | - } |
293 | | - } |
294 | | - |
295 | | - // by not filling out the stack trace, we gain speed when using the exception as flow control |
296 | | - private static boolean hasZeroArgs(Method mth) { |
297 | | - return mth.getParameterCount() == 0; |
298 | | - } |
299 | | - |
300 | | - private static boolean takesDataFetcherEnvironmentAsOnlyArgument(Method mth) { |
301 | | - return mth.getParameterCount() == 1 && |
302 | | - mth.getParameterTypes()[0].equals(DataFetchingEnvironment.class); |
303 | | - } |
304 | | - |
305 | | - private static Comparator<? super Method> mostMethodArgsFirst() { |
306 | | - return Comparator.comparingInt(Method::getParameterCount).reversed(); |
307 | | - } |
308 | | - |
309 | | - @SuppressWarnings("serial") |
310 | | - private static class FastNoSuchMethodException extends NoSuchMethodException { |
311 | | - public FastNoSuchMethodException(String methodName) { |
312 | | - super(methodName); |
313 | | - } |
314 | | - |
315 | | - @Override |
316 | | - public synchronized Throwable fillInStackTrace() { |
317 | | - return this; |
318 | | - } |
| 31 | + return impl.setUseNegativeCache(flag); |
319 | 32 | } |
320 | 33 | } |
0 commit comments