Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8

bintray.user=DUMMY_USER
bintray.key=DUMMY_KEY
version = 5.3
version = 5.3.1
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,35 @@

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static graphql.annotations.processor.util.NamingKit.toGraphqlName;
import static graphql.annotations.processor.util.PrefixesUtil.addPrefixToPropertyName;
import static graphql.annotations.processor.util.PrefixesUtil.extractPrefixedName;
import static graphql.annotations.processor.util.ReflectionKit.constructNewInstance;
import static graphql.annotations.processor.util.ReflectionKit.newInstance;


/**
* This class is determining how to return value of a method from an api entity
* The order of the mapping:
* 1. If no source is provided to map between - invoking the method implementation
* 2. If annotated with @GraphQLInvokeDetached - invoking the method implementation
* 3. else If source is provided, and method name is matching a method name in the source object - execute source implementation
* i.e method name is: `name` ; existing method in the source object with name: `name`
* 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
* i.e method name is: `name` ; existing method in the source object with name: `getName`
* 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
* i.e method name is: `name` ; existing method in the source object with name: isName
* 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
* i.e method name is: `name` ; field name in source object is: `name`
* 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
* i.e method name is: `getName` ; field name in source object is: `name`
*
* @param <T> type of the returned value
*/
public class MethodDataFetcher<T> implements DataFetcher<T> {
private final Method method;
private final ProcessingElementsContainer container;
Expand All @@ -47,7 +68,10 @@ public MethodDataFetcher(Method method, TypeFunction typeFunction, ProcessingEle
public T get(DataFetchingEnvironment environment) {
try {
T obj;
if (method.isAnnotationPresent(GraphQLBatched.class) || method.isAnnotationPresent(GraphQLInvokeDetached.class)) {
if (Modifier.isStatic(method.getModifiers())){
return (T) method.invoke(null, invocationArgs(environment, container));
}
else if (method.isAnnotationPresent(GraphQLBatched.class) || method.isAnnotationPresent(GraphQLInvokeDetached.class)) {
obj = newInstance((Class<T>) method.getDeclaringClass());
} else if (!method.getDeclaringClass().isInstance(environment.getSource())) {
obj = newInstance((Class<T>) method.getDeclaringClass(), environment.getSource());
Expand All @@ -59,7 +83,7 @@ public T get(DataFetchingEnvironment environment) {
}

if (obj == null && environment.getSource() != null) {
Object value = getGraphQLFieldValue(environment.getSource(), environment.getField().getName());
Object value = getGraphQLFieldValue(environment.getSource(), method.getName());
return (T) value;
}

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

Field field = getField(source.getClass(), fieldName);
if (getValueFromField(field)) return field.get(source);
Object fieldValue = getValueFromField(source, fieldName);
if (fieldValue != null) return fieldValue;

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

private boolean getValueFromField(Field field) throws IllegalAccessException {
private Object getValueFromField(Object source, String fieldName) throws IllegalAccessException {
List<String> namesToSearchFor = Arrays.asList(fieldName, extractPrefixedName(fieldName));
for (String name : namesToSearchFor) {
Field field = getField(source.getClass(), name);
if (isFieldContainsValue(field)) {
return field.get(source);
}
}
return null;
}

private boolean isFieldContainsValue(Field field) throws IllegalAccessException {
if (field != null) {
field.setAccessible(true);
return true;
}
return false;
}

private Field getField(Class<?> clazz, String name) {
Field field = null;
while (clazz != null && field == null) {
try {
field = clazz.getDeclaredField(name);
} catch (Exception ignored) {
}
clazz = clazz.getSuperclass();
}
return field;
}

private Object getValueFromMethod(Object source, String fieldName) throws IllegalAccessException, InvocationTargetException {
String[] orderedPrefixes = new String[]{"", "get", "is"};
for (String orderedPrefix : orderedPrefixes) {
Expand All @@ -166,7 +213,13 @@ private Object getValueFromMethod(Object source, String fieldName) throws Illega
}

private Method getMethod(Class<?> clazz, String name, String prefix) {
String prefixedName = addPrefixToPropertyName(prefix, name);
String prefixedName;
if (prefix.isEmpty()) {
prefixedName = name;
} else {
prefixedName = addPrefixToPropertyName(prefix, name);
}

Method method = null;
while (clazz != null && method == null) {
try {
Expand All @@ -179,16 +232,4 @@ private Method getMethod(Class<?> clazz, String name, String prefix) {
return method;
}

private Field getField(Class<?> clazz, String name) {
Field field = null;
while (clazz != null && field == null) {
try {
field = clazz.getDeclaredField(name);
} catch (Exception ignored) {
}
clazz = clazz.getSuperclass();
}
return field;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,13 @@ public class PrefixesUtil {
public static String addPrefixToPropertyName(String prefix, String propertyName) {
return prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
}

public static String extractPrefixedName(String name) {
if (name.startsWith("is")) {
return name.replaceFirst("^is", "").toLowerCase();
} else if (name.startsWith("get")) {
return name.replaceFirst("^get", "").toLowerCase();
}
return name;
}
}
143 changes: 132 additions & 11 deletions src/test/java/graphql/annotations/MethodDataFetcherTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import javax.xml.crypto.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -41,6 +40,24 @@ public void init() {
GraphQLAnnotations.getInstance().getTypeRegistry().clear();
}

public static class StaticApi {
@GraphQLField
public static String name() {
return "osher";
}
}

@Test
public void query_staticMethod_valueIsDeterminedByMethod(){
GraphQLObjectType object = GraphQLAnnotations.object(StaticApi.class);
GraphQLSchema schema = newSchema().query(object).build();

ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { name }").root(new StaticApi()));
assertTrue(result.getErrors().isEmpty());
assertEquals(((Map<String, String>) result.getData()).get("name").toString(), "osher");
}


/**
* CASE 1 : Only Api class, value determined by field
*/
Expand Down Expand Up @@ -95,10 +112,12 @@ public void query_onlyApiClass_valueIsDeterminedByMethod() throws Exception {
}

/**
* Case 3: Api and a DB class, value is determined by the db field
* Case 3: Api and a DB class with polymorphism, value is determined by the db field
* name of api method <-> name of db field
*/
public static class Api3 {
@GraphQLField
@GraphQLName("nameX")
public String name() {
return "dani";
}
Expand Down Expand Up @@ -131,17 +150,19 @@ public void query_apiAndDbClass_valueIsDeterminedByDBField() throws Exception {
GraphQLObjectType object = GraphQLAnnotations.object(Query3.class);
GraphQLSchema schema = newSchema().query(object).build();

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

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

public static class Api4 {
@GraphQLField
@GraphQLName("nameX")
public String name() {
return null;
}
Expand Down Expand Up @@ -173,15 +194,116 @@ public static class Query4 {
}

@Test
public void query_apiAndDbClass_valueIsDeterminedByDBMethod() throws Exception {
public void query_apiAndDbClass_valueIsDeterminedByGetPrefixDBMethod() throws Exception {
GraphQLObjectType object = GraphQLAnnotations.object(Query4.class);
GraphQLSchema schema = newSchema().query(object).build();

ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { name } }").root(new Query4()));
ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { nameX } }").root(new Query4()));
assertTrue(result.getErrors().isEmpty());
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("nameX").toString(), "guy/yarin");
}

/**
* Case: Api and DB classes, value is determined by db method
* api method name <-> (`is`) + db method name
*/

public static class Api6 {
@GraphQLField
@GraphQLName("nameX")
public String name() {
return null;
}
}

public static class SuperDB6 {
private String name = "guy";

public String isName() {
return name + "/yarin";
}
}

public static class DB6 extends SuperDB6 {
}

public static class Api6Resolver implements DataFetcher<DB6> {

@Override
public DB6 get(DataFetchingEnvironment environment) {
return new DB6();
}
}

public static class Query6 {
@GraphQLField
@GraphQLDataFetcher(Api6Resolver.class)
public Api6 queryField;
}

@Test
public void query_apiAndDbClass_valueIsDeterminedByIsPrefixDBMethod() throws Exception {
GraphQLObjectType object = GraphQLAnnotations.object(Query6.class);
GraphQLSchema schema = newSchema().query(object).build();

ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { nameX } }").root(new Query6()));
assertTrue(result.getErrors().isEmpty());
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("name").toString(), "guy/yarin");
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("nameX").toString(), "guy/yarin");
}

/**
* Case: Api and DB classes, value is determined by db method
* api method name <-> db method name
*/

public static class Api7 {
@GraphQLField
@GraphQLName("nameX")
public String name() {
return null;
}
}

public static class SuperDB7 {
private String name = "guy";

public String name() {
return name + "/yarin";
}

public String isName() {
return "blabla";
}
}

public static class DB7 extends SuperDB7 {
}

public static class Api7Resolver implements DataFetcher<DB7> {

@Override
public DB7 get(DataFetchingEnvironment environment) {
return new DB7();
}
}

public static class Query7 {
@GraphQLField
@GraphQLDataFetcher(Api7Resolver.class)
public Api7 queryField;
}

@Test
public void query_apiAndDbClass_valueIsDeterminedByDBMethod() throws Exception {
GraphQLObjectType object = GraphQLAnnotations.object(Query7.class);
GraphQLSchema schema = newSchema().query(object).build();

ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { nameX } }").root(new Query7()));
assertTrue(result.getErrors().isEmpty());
assertEquals(((Map<String, Map<String, String>>) result.getData()).get("queryField").get("nameX").toString(), "guy/yarin");
}


/**
* Case 5: Invoke Detached on method, both api and db classes, value is determined by the api method
*/
Expand Down Expand Up @@ -215,8 +337,6 @@ public static class Query5 {
public Api5 queryField;
}

/////////////////////////////////////////

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

/////////////////////////////////////////
/////////////////////////////////////////
/////////////////////////////////////////

public class TestException extends Exception {
}
Expand Down Expand Up @@ -276,8 +399,6 @@ public int c() {
public CanonizedTypeApi getCanonizedType() {
return null;
}


}

public static class CanonizedFetcher implements DataFetcher<CanonizedType> {
Expand Down