Skip to content

Commit 8a86a43

Browse files
authored
Merge pull request #179 from yarinvak/methoddatafetcher-methodnames-fix
Methoddatafetcher methodnames fix
2 parents 6f33220 + b6bc72d commit 8a86a43

File tree

4 files changed

+201
-30
lines changed

4 files changed

+201
-30
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8
77

88
bintray.user=DUMMY_USER
99
bintray.key=DUMMY_KEY
10-
version = 5.3
10+
version = 5.3.1

src/main/java/graphql/annotations/dataFetchers/MethodDataFetcher.java

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,35 @@
2323

2424
import java.lang.reflect.*;
2525
import java.util.ArrayList;
26+
import java.util.Arrays;
2627
import java.util.List;
2728
import java.util.Map;
2829

2930
import static graphql.annotations.processor.util.NamingKit.toGraphqlName;
3031
import static graphql.annotations.processor.util.PrefixesUtil.addPrefixToPropertyName;
32+
import static graphql.annotations.processor.util.PrefixesUtil.extractPrefixedName;
3133
import static graphql.annotations.processor.util.ReflectionKit.constructNewInstance;
3234
import static graphql.annotations.processor.util.ReflectionKit.newInstance;
3335

36+
37+
/**
38+
* This class is determining how to return value of a method from an api entity
39+
* The order of the mapping:
40+
* 1. If no source is provided to map between - invoking the method implementation
41+
* 2. If annotated with @GraphQLInvokeDetached - invoking the method implementation
42+
* 3. else If source is provided, and method name is matching a method name in the source object - execute source implementation
43+
* i.e method name is: `name` ; existing method in the source object with name: `name`
44+
* 4. else If source is provided, and method name is matching a method name with a `get` prefix in the source object - execute source implementation
45+
* i.e method name is: `name` ; existing method in the source object with name: `getName`
46+
* 5. else If source is provided, and method name is matching a method name with a `is` prefix in the source object - execute source implementation
47+
* i.e method name is: `name` ; existing method in the source object with name: isName
48+
* 6. else If source is provided, and method name is matching a field name in the source object - return field value from the source object
49+
* i.e method name is: `name` ; field name in source object is: `name`
50+
* 7. else If source is provided, and method name is prefixed with `get` or `is` - and it matches to a field name (without the prefix) in the source object - return field value from the source object
51+
* i.e method name is: `getName` ; field name in source object is: `name`
52+
*
53+
* @param <T> type of the returned value
54+
*/
3455
public class MethodDataFetcher<T> implements DataFetcher<T> {
3556
private final Method method;
3657
private final ProcessingElementsContainer container;
@@ -47,7 +68,10 @@ public MethodDataFetcher(Method method, TypeFunction typeFunction, ProcessingEle
4768
public T get(DataFetchingEnvironment environment) {
4869
try {
4970
T obj;
50-
if (method.isAnnotationPresent(GraphQLBatched.class) || method.isAnnotationPresent(GraphQLInvokeDetached.class)) {
71+
if (Modifier.isStatic(method.getModifiers())){
72+
return (T) method.invoke(null, invocationArgs(environment, container));
73+
}
74+
else if (method.isAnnotationPresent(GraphQLBatched.class) || method.isAnnotationPresent(GraphQLInvokeDetached.class)) {
5175
obj = newInstance((Class<T>) method.getDeclaringClass());
5276
} else if (!method.getDeclaringClass().isInstance(environment.getSource())) {
5377
obj = newInstance((Class<T>) method.getDeclaringClass(), environment.getSource());
@@ -59,7 +83,7 @@ public T get(DataFetchingEnvironment environment) {
5983
}
6084

6185
if (obj == null && environment.getSource() != null) {
62-
Object value = getGraphQLFieldValue(environment.getSource(), environment.getField().getName());
86+
Object value = getGraphQLFieldValue(environment.getSource(), method.getName());
6387
return (T) value;
6488
}
6589

@@ -140,20 +164,43 @@ private Object getGraphQLFieldValue(Object source, String fieldName) throws Ille
140164
Object methodValue = getValueFromMethod(source, fieldName);
141165
if (methodValue != null) return methodValue;
142166

143-
Field field = getField(source.getClass(), fieldName);
144-
if (getValueFromField(field)) return field.get(source);
167+
Object fieldValue = getValueFromField(source, fieldName);
168+
if (fieldValue != null) return fieldValue;
145169

146170
throw new NoSuchFieldException("No GraphQL field found");
147171
}
148172

149-
private boolean getValueFromField(Field field) throws IllegalAccessException {
173+
private Object getValueFromField(Object source, String fieldName) throws IllegalAccessException {
174+
List<String> namesToSearchFor = Arrays.asList(fieldName, extractPrefixedName(fieldName));
175+
for (String name : namesToSearchFor) {
176+
Field field = getField(source.getClass(), name);
177+
if (isFieldContainsValue(field)) {
178+
return field.get(source);
179+
}
180+
}
181+
return null;
182+
}
183+
184+
private boolean isFieldContainsValue(Field field) throws IllegalAccessException {
150185
if (field != null) {
151186
field.setAccessible(true);
152187
return true;
153188
}
154189
return false;
155190
}
156191

192+
private Field getField(Class<?> clazz, String name) {
193+
Field field = null;
194+
while (clazz != null && field == null) {
195+
try {
196+
field = clazz.getDeclaredField(name);
197+
} catch (Exception ignored) {
198+
}
199+
clazz = clazz.getSuperclass();
200+
}
201+
return field;
202+
}
203+
157204
private Object getValueFromMethod(Object source, String fieldName) throws IllegalAccessException, InvocationTargetException {
158205
String[] orderedPrefixes = new String[]{"", "get", "is"};
159206
for (String orderedPrefix : orderedPrefixes) {
@@ -166,7 +213,13 @@ private Object getValueFromMethod(Object source, String fieldName) throws Illega
166213
}
167214

168215
private Method getMethod(Class<?> clazz, String name, String prefix) {
169-
String prefixedName = addPrefixToPropertyName(prefix, name);
216+
String prefixedName;
217+
if (prefix.isEmpty()) {
218+
prefixedName = name;
219+
} else {
220+
prefixedName = addPrefixToPropertyName(prefix, name);
221+
}
222+
170223
Method method = null;
171224
while (clazz != null && method == null) {
172225
try {
@@ -179,16 +232,4 @@ private Method getMethod(Class<?> clazz, String name, String prefix) {
179232
return method;
180233
}
181234

182-
private Field getField(Class<?> clazz, String name) {
183-
Field field = null;
184-
while (clazz != null && field == null) {
185-
try {
186-
field = clazz.getDeclaredField(name);
187-
} catch (Exception ignored) {
188-
}
189-
clazz = clazz.getSuperclass();
190-
}
191-
return field;
192-
}
193-
194235
}

src/main/java/graphql/annotations/processor/util/PrefixesUtil.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,13 @@ public class PrefixesUtil {
1818
public static String addPrefixToPropertyName(String prefix, String propertyName) {
1919
return prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
2020
}
21+
22+
public static String extractPrefixedName(String name) {
23+
if (name.startsWith("is")) {
24+
return name.replaceFirst("^is", "").toLowerCase();
25+
} else if (name.startsWith("get")) {
26+
return name.replaceFirst("^get", "").toLowerCase();
27+
}
28+
return name;
29+
}
2130
}

src/test/java/graphql/annotations/MethodDataFetcherTest.java

Lines changed: 132 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.testng.annotations.BeforeMethod;
2626
import org.testng.annotations.Test;
2727

28-
import javax.xml.crypto.Data;
2928
import java.util.ArrayList;
3029
import java.util.HashMap;
3130
import java.util.Map;
@@ -41,6 +40,24 @@ public void init() {
4140
GraphQLAnnotations.getInstance().getTypeRegistry().clear();
4241
}
4342

43+
public static class StaticApi {
44+
@GraphQLField
45+
public static String name() {
46+
return "osher";
47+
}
48+
}
49+
50+
@Test
51+
public void query_staticMethod_valueIsDeterminedByMethod(){
52+
GraphQLObjectType object = GraphQLAnnotations.object(StaticApi.class);
53+
GraphQLSchema schema = newSchema().query(object).build();
54+
55+
ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { name }").root(new StaticApi()));
56+
assertTrue(result.getErrors().isEmpty());
57+
assertEquals(((Map<String, String>) result.getData()).get("name").toString(), "osher");
58+
}
59+
60+
4461
/**
4562
* CASE 1 : Only Api class, value determined by field
4663
*/
@@ -95,10 +112,12 @@ public void query_onlyApiClass_valueIsDeterminedByMethod() throws Exception {
95112
}
96113

97114
/**
98-
* Case 3: Api and a DB class, value is determined by the db field
115+
* Case 3: Api and a DB class with polymorphism, value is determined by the db field
116+
* name of api method <-> name of db field
99117
*/
100118
public static class Api3 {
101119
@GraphQLField
120+
@GraphQLName("nameX")
102121
public String name() {
103122
return "dani";
104123
}
@@ -131,17 +150,19 @@ public void query_apiAndDbClass_valueIsDeterminedByDBField() throws Exception {
131150
GraphQLObjectType object = GraphQLAnnotations.object(Query3.class);
132151
GraphQLSchema schema = newSchema().query(object).build();
133152

134-
ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { name } }").root(new Query3()));
153+
ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { nameX } }").root(new Query3()));
135154
assertTrue(result.getErrors().isEmpty());
136-
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("name").toString(), "osher");
155+
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("nameX").toString(), "osher");
137156
}
138157

139158
/**
140159
* Case 4: Api and DB classes, value is determined by db method
160+
* api method name <-> (`get`) + db method name
141161
*/
142162

143163
public static class Api4 {
144164
@GraphQLField
165+
@GraphQLName("nameX")
145166
public String name() {
146167
return null;
147168
}
@@ -173,15 +194,116 @@ public static class Query4 {
173194
}
174195

175196
@Test
176-
public void query_apiAndDbClass_valueIsDeterminedByDBMethod() throws Exception {
197+
public void query_apiAndDbClass_valueIsDeterminedByGetPrefixDBMethod() throws Exception {
177198
GraphQLObjectType object = GraphQLAnnotations.object(Query4.class);
178199
GraphQLSchema schema = newSchema().query(object).build();
179200

180-
ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { name } }").root(new Query4()));
201+
ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { nameX } }").root(new Query4()));
202+
assertTrue(result.getErrors().isEmpty());
203+
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("nameX").toString(), "guy/yarin");
204+
}
205+
206+
/**
207+
* Case: Api and DB classes, value is determined by db method
208+
* api method name <-> (`is`) + db method name
209+
*/
210+
211+
public static class Api6 {
212+
@GraphQLField
213+
@GraphQLName("nameX")
214+
public String name() {
215+
return null;
216+
}
217+
}
218+
219+
public static class SuperDB6 {
220+
private String name = "guy";
221+
222+
public String isName() {
223+
return name + "/yarin";
224+
}
225+
}
226+
227+
public static class DB6 extends SuperDB6 {
228+
}
229+
230+
public static class Api6Resolver implements DataFetcher<DB6> {
231+
232+
@Override
233+
public DB6 get(DataFetchingEnvironment environment) {
234+
return new DB6();
235+
}
236+
}
237+
238+
public static class Query6 {
239+
@GraphQLField
240+
@GraphQLDataFetcher(Api6Resolver.class)
241+
public Api6 queryField;
242+
}
243+
244+
@Test
245+
public void query_apiAndDbClass_valueIsDeterminedByIsPrefixDBMethod() throws Exception {
246+
GraphQLObjectType object = GraphQLAnnotations.object(Query6.class);
247+
GraphQLSchema schema = newSchema().query(object).build();
248+
249+
ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { nameX } }").root(new Query6()));
181250
assertTrue(result.getErrors().isEmpty());
182-
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("name").toString(), "guy/yarin");
251+
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("nameX").toString(), "guy/yarin");
183252
}
184253

254+
/**
255+
* Case: Api and DB classes, value is determined by db method
256+
* api method name <-> db method name
257+
*/
258+
259+
public static class Api7 {
260+
@GraphQLField
261+
@GraphQLName("nameX")
262+
public String name() {
263+
return null;
264+
}
265+
}
266+
267+
public static class SuperDB7 {
268+
private String name = "guy";
269+
270+
public String name() {
271+
return name + "/yarin";
272+
}
273+
274+
public String isName() {
275+
return "blabla";
276+
}
277+
}
278+
279+
public static class DB7 extends SuperDB7 {
280+
}
281+
282+
public static class Api7Resolver implements DataFetcher<DB7> {
283+
284+
@Override
285+
public DB7 get(DataFetchingEnvironment environment) {
286+
return new DB7();
287+
}
288+
}
289+
290+
public static class Query7 {
291+
@GraphQLField
292+
@GraphQLDataFetcher(Api7Resolver.class)
293+
public Api7 queryField;
294+
}
295+
296+
@Test
297+
public void query_apiAndDbClass_valueIsDeterminedByDBMethod() throws Exception {
298+
GraphQLObjectType object = GraphQLAnnotations.object(Query7.class);
299+
GraphQLSchema schema = newSchema().query(object).build();
300+
301+
ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { nameX } }").root(new Query7()));
302+
assertTrue(result.getErrors().isEmpty());
303+
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("nameX").toString(), "guy/yarin");
304+
}
305+
306+
185307
/**
186308
* Case 5: Invoke Detached on method, both api and db classes, value is determined by the api method
187309
*/
@@ -215,8 +337,6 @@ public static class Query5 {
215337
public Api5 queryField;
216338
}
217339

218-
/////////////////////////////////////////
219-
220340
@Test
221341
public void query_apiAndDbClassAndApiIsInvokeDetached_valueIsDeterminedByApiMethod() throws Exception {
222342
GraphQLObjectType object = GraphQLAnnotations.object(Query5.class);
@@ -227,6 +347,9 @@ public void query_apiAndDbClassAndApiIsInvokeDetached_valueIsDeterminedByApiMeth
227347
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("name").toString(), "yarin/guy/osher");
228348
}
229349

350+
/////////////////////////////////////////
351+
/////////////////////////////////////////
352+
/////////////////////////////////////////
230353

231354
public class TestException extends Exception {
232355
}
@@ -276,8 +399,6 @@ public int c() {
276399
public CanonizedTypeApi getCanonizedType() {
277400
return null;
278401
}
279-
280-
281402
}
282403

283404
public static class CanonizedFetcher implements DataFetcher<CanonizedType> {

0 commit comments

Comments
 (0)