diff --git a/gradle.properties b/gradle.properties index dbcf7256..a3c753b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8 bintray.user=DUMMY_USER bintray.key=DUMMY_KEY -version = 5.2 +version = 5.3 \ No newline at end of file diff --git a/secret2.gpg b/secret2.gpg new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/graphql/annotations/dataFetchers/MethodDataFetcher.java b/src/main/java/graphql/annotations/dataFetchers/MethodDataFetcher.java index 9dbaa281..c7a657c7 100644 --- a/src/main/java/graphql/annotations/dataFetchers/MethodDataFetcher.java +++ b/src/main/java/graphql/annotations/dataFetchers/MethodDataFetcher.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -27,6 +27,7 @@ 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.ReflectionKit.constructNewInstance; import static graphql.annotations.processor.util.ReflectionKit.newInstance; @@ -58,7 +59,7 @@ public T get(DataFetchingEnvironment environment) { } if (obj == null && environment.getSource() != null) { - Object value = getFieldValue(environment.getSource(), method.getName()); + Object value = getGraphQLFieldValue(environment.getSource(), environment.getField().getName()); return (T) value; } @@ -135,9 +136,59 @@ private Object buildArg(Type p, GraphQLType graphQLType, Object arg) { } } - private Object getFieldValue(Object source, String fieldName) throws IllegalAccessException, NoSuchFieldException { - Field field = source.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.get(source); + private Object getGraphQLFieldValue(Object source, String fieldName) throws IllegalAccessException, NoSuchFieldException, InvocationTargetException { + Object methodValue = getValueFromMethod(source, fieldName); + if (methodValue != null) return methodValue; + + Field field = getField(source.getClass(), fieldName); + if (getValueFromField(field)) return field.get(source); + + throw new NoSuchFieldException("No GraphQL field found"); + } + + private boolean getValueFromField(Field field) throws IllegalAccessException { + if (field != null) { + field.setAccessible(true); + return true; + } + return false; } + + private Object getValueFromMethod(Object source, String fieldName) throws IllegalAccessException, InvocationTargetException { + String[] orderedPrefixes = new String[]{"", "get", "is"}; + for (String orderedPrefix : orderedPrefixes) { + Method method = getMethod(source.getClass(), fieldName, orderedPrefix); + if (method != null) { + return method.invoke(source); + } + } + return null; + } + + private Method getMethod(Class clazz, String name, String prefix) { + String prefixedName = addPrefixToPropertyName(prefix, name); + Method method = null; + while (clazz != null && method == null) { + try { + method = clazz.getDeclaredMethod(prefixedName); + } catch (Exception ignored) { + } + clazz = clazz.getSuperclass(); + } + + 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; + } + } diff --git a/src/main/java/graphql/annotations/processor/retrievers/fieldBuilders/method/MethodNameBuilder.java b/src/main/java/graphql/annotations/processor/retrievers/fieldBuilders/method/MethodNameBuilder.java index b4ba519c..028b0ec8 100644 --- a/src/main/java/graphql/annotations/processor/retrievers/fieldBuilders/method/MethodNameBuilder.java +++ b/src/main/java/graphql/annotations/processor/retrievers/fieldBuilders/method/MethodNameBuilder.java @@ -32,13 +32,13 @@ public MethodNameBuilder(Method method) { @Override public String build() { if (method.isAnnotationPresent(GraphQLPrettify.class) && !method.isAnnotationPresent(GraphQLName.class)) { - return toGraphqlName(pretifyName(method.getName())); + return toGraphqlName(prettifyName(method.getName())); } GraphQLName name = method.getAnnotation(GraphQLName.class); return toGraphqlName(name == null ? method.getName() : name.value()); } - private String pretifyName(String originalName) { + private String prettifyName(String originalName) { String name = originalName.replaceFirst("^(is|get|set)(.+)", "$2"); return Character.toLowerCase(name.charAt(0)) + name.substring(1); } diff --git a/src/main/java/graphql/annotations/processor/util/PrefixesUtil.java b/src/main/java/graphql/annotations/processor/util/PrefixesUtil.java new file mode 100644 index 00000000..d7d69603 --- /dev/null +++ b/src/main/java/graphql/annotations/processor/util/PrefixesUtil.java @@ -0,0 +1,21 @@ +/** + * Copyright 2016 Yurii Rashkovskii + * + * 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 + */ +package graphql.annotations.processor.util; + +public class PrefixesUtil { + public static String addPrefixToPropertyName(String prefix, String propertyName) { + return prefix + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1); + } +} diff --git a/src/test/java/graphql/annotations/MethodDataFetcherTest.java b/src/test/java/graphql/annotations/MethodDataFetcherTest.java index 3bdcc37b..bbff4492 100644 --- a/src/test/java/graphql/annotations/MethodDataFetcherTest.java +++ b/src/test/java/graphql/annotations/MethodDataFetcherTest.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -17,9 +17,7 @@ import graphql.ExceptionWhileDataFetching; import graphql.ExecutionResult; import graphql.GraphQL; -import graphql.annotations.annotationTypes.GraphQLDataFetcher; -import graphql.annotations.annotationTypes.GraphQLField; -import graphql.annotations.annotationTypes.GraphQLInvokeDetached; +import graphql.annotations.annotationTypes.*; import graphql.annotations.annotationTypes.GraphQLType; import graphql.annotations.dataFetchers.MethodDataFetcher; import graphql.annotations.processor.GraphQLAnnotations; @@ -27,6 +25,7 @@ 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; @@ -42,6 +41,192 @@ public void init() { GraphQLAnnotations.getInstance().getTypeRegistry().clear(); } + /** + * CASE 1 : Only Api class, value determined by field + */ + public static class Api1 { + @GraphQLField + private String name = "yarin"; + } + + public static class Query1 { + @GraphQLField + public Api1 queryField() { + return new Api1(); + } + } + + @Test + public void query_onlyApiClass_valueIsDeterminedByField() throws Exception { + GraphQLObjectType object = GraphQLAnnotations.object(Query1.class); + GraphQLSchema schema = newSchema().query(object).build(); + + ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { name } }").root(new Query1())); + assertTrue(result.getErrors().isEmpty()); + assertEquals(((Map>) result.getData()).get("queryField").get("name").toString(), "yarin"); + } + + + /** + * CASE 2 : Only Api class, value determined by method + */ + public static class Api2 { + @GraphQLField + public String name() { + return "guy"; + } + } + + public static class Query2 { + @GraphQLField + public Api2 queryField() { + return new Api2(); + } + } + + @Test + public void query_onlyApiClass_valueIsDeterminedByMethod() throws Exception { + GraphQLObjectType object = GraphQLAnnotations.object(Query2.class); + GraphQLSchema schema = newSchema().query(object).build(); + + ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { name } }").root(new Query2())); + assertTrue(result.getErrors().isEmpty()); + assertEquals(((Map>) result.getData()).get("queryField").get("name").toString(), "guy"); + } + + /** + * Case 3: Api and a DB class, value is determined by the db field + */ + public static class Api3 { + @GraphQLField + public String name() { + return "dani"; + } + } + + public static class SuperDb3 { + private String name = "osher"; + + } + + public static class DB3 extends SuperDb3 { + } + + public static class Api3Resolver implements DataFetcher { + + @Override + public DB3 get(DataFetchingEnvironment environment) { + return new DB3(); + } + } + + public static class Query3 { + @GraphQLField + @GraphQLDataFetcher(Api3Resolver.class) + public Api3 queryField; + } + + @Test + 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())); + assertTrue(result.getErrors().isEmpty()); + assertEquals(((Map>) result.getData()).get("queryField").get("name").toString(), "osher"); + } + + /** + * Case 4: Api and DB classes, value is determined by db method + */ + + public static class Api4 { + @GraphQLField + public String name() { + return null; + } + } + + public static class SuperDB4 { + private String name = "guy"; + + public String getName() { + return name + "/yarin"; + } + } + + public static class DB4 extends SuperDB4 { + } + + public static class Api4Resolver implements DataFetcher { + + @Override + public DB4 get(DataFetchingEnvironment environment) { + return new DB4(); + } + } + + public static class Query4 { + @GraphQLField + @GraphQLDataFetcher(Api4Resolver.class) + public Api4 queryField; + } + + @Test + public void query_apiAndDbClass_valueIsDeterminedByDBMethod() 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())); + assertTrue(result.getErrors().isEmpty()); + assertEquals(((Map>) result.getData()).get("queryField").get("name").toString(), "guy/yarin"); + } + + /** + * Case 5: Invoke Detached on method, both api and db classes, value is determined by the api method + */ + + public static class Api5 { + private String name = "yarin"; + + @GraphQLField + @GraphQLInvokeDetached + @GraphQLPrettify + public String getName() { + return name + "/guy/osher"; + } + } + + public static class DB5 { + private String name = "moshe"; + } + + public static class Api5Resolver implements DataFetcher { + + @Override + public DB5 get(DataFetchingEnvironment environment) { + return new DB5(); + } + } + + public static class Query5 { + @GraphQLField + @GraphQLDataFetcher(Api5Resolver.class) + public Api5 queryField; + } + +///////////////////////////////////////// + + @Test + public void query_apiAndDbClassAndApiIsInvokeDetached_valueIsDeterminedByApiMethod() throws Exception { + GraphQLObjectType object = GraphQLAnnotations.object(Query5.class); + GraphQLSchema schema = newSchema().query(object).build(); + + ExecutionResult result = GraphQL.newGraphQL(schema).build().execute(builder -> builder.query("query { queryField { name } }").root(new Query5())); + assertTrue(result.getErrors().isEmpty()); + assertEquals(((Map>) result.getData()).get("queryField").get("name").toString(), "yarin/guy/osher"); + } + public class TestException extends Exception { } @@ -68,6 +253,12 @@ public int a() { return 1; } + @GraphQLField + @GraphQLPrettify + public int getX() { + return 1; + } + @GraphQLField @GraphQLInvokeDetached public int b() { @@ -78,11 +269,41 @@ public int b() { public int c() { return 4; } + + @GraphQLField + @GraphQLPrettify + @GraphQLDataFetcher(CanonizedFetcher.class) + public CanonizedTypeApi getCanonizedType() { + return null; + } + + + } + + public static class CanonizedFetcher implements DataFetcher { + + @Override + public CanonizedType get(DataFetchingEnvironment environment) { + return new CanonizedType(); + } + } + + public static class CanonizedTypeApi { + @GraphQLPrettify + @GraphQLField + public int getM() { + return 1; + } + } + + public static class CanonizedType { + public int m = 5; } public static class InternalType { public int a = 123; public int b; + public int x = 5; } @GraphQLType @@ -119,6 +340,27 @@ public void queryingOneFieldNotAnnotatedWithGraphQLInvokeDetached_valueIsDetermi assertEquals(((Map>) result.getData()).get("field").get("a").toString(), "123"); } + + @Test + public void queryingOneCanonizedFieldNotAnnotatedWithGraphQLInvokeDetached_valueIsDeterminedByEntity() { + GraphQLObjectType object = GraphQLAnnotations.object(Query.class); + GraphQLSchema schema = newSchema().query(object).build(); + + ExecutionResult result = GraphQL.newGraphQL(schema).build().execute("query { field { canonizedType { m } } }"); + assertTrue(result.getErrors().isEmpty()); + assertEquals(((Map>>) result.getData()).get("field").get("canonizedType").get("m").toString(), "5"); + } + + @Test + public void queryingOneFieldNotAnnotatedWithGraphQLInvokeDetachedAndNameIsPrettified_valueIsDeterminedByEntity() { + GraphQLObjectType object = GraphQLAnnotations.object(Query.class); + GraphQLSchema schema = newSchema().query(object).build(); + + ExecutionResult result = GraphQL.newGraphQL(schema).build().execute("query { field { x } }"); + assertTrue(result.getErrors().isEmpty()); + assertEquals(((Map>) result.getData()).get("field").get("x").toString(), "5"); + } + @Test public void queryingOneFieldAnnotatedWithGraphQLInvokeDetached_valueIsDeterminedByApiEntity() { GraphQLObjectType object = GraphQLAnnotations.object(Query.class); @@ -141,12 +383,12 @@ public void queryingFieldsFromApiEntityFetcher_valueIsDeterminedByApiEntity() { } @Test - public void queryingFieldsFromNoApiEntityFetcher_noMatchingFieldInEntity_throwException(){ + public void queryingFieldsFromNoApiEntityFetcher_noMatchingFieldInEntity_throwException() { GraphQLObjectType object = GraphQLAnnotations.object(Query.class); GraphQLSchema schema = newSchema().query(object).build(); ExecutionResult result = GraphQL.newGraphQL(schema).build().execute("query { field { c } }"); assertFalse(result.getErrors().isEmpty()); - assertTrue(((ExceptionWhileDataFetching)result.getErrors().get(0)).getException().getCause() instanceof NoSuchFieldException); + assertTrue(((ExceptionWhileDataFetching) result.getErrors().get(0)).getException().getCause() instanceof NoSuchFieldException); } } \ No newline at end of file