diff --git a/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLDescription.java b/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLDescription.java index 1f3e2a99e..da92beb6d 100644 --- a/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLDescription.java +++ b/graphql-jpa-query-annotations/src/main/java/com/introproventures/graphql/jpa/query/annotation/GraphQLDescription.java @@ -17,6 +17,7 @@ import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; @@ -28,7 +29,7 @@ * @author Igor Dianov * */ -@Target( { TYPE, FIELD }) +@Target( { TYPE, FIELD, METHOD }) @Retention(RUNTIME) public @interface GraphQLDescription { diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 61b8f921a..70f5d1a78 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -16,9 +16,14 @@ package com.introproventures.graphql.jpa.query.schema.impl; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Member; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -562,7 +567,68 @@ protected final boolean isValidInput(Attribute attribute) { private String getSchemaDescription(Member member) { if (member instanceof AnnotatedElement) { - return getSchemaDescription((AnnotatedElement) member); + String desc = getSchemaDescription((AnnotatedElement) member); + if (desc != null) { + return(desc); + } + } + + //The given Member has no @GraphQLDescription set. + //If the Member is a Method it might be a getter/setter, see if the property it represents + //is annotated with @GraphQLDescription + //Alternatively if the Member is a Field its getter might be annotated, see if its getter + //is annotated with @GraphQLDescription + if (member instanceof Method) { + Field fieldMember = getFieldByAccessor((Method)member); + if (fieldMember != null) { + return(getSchemaDescription((AnnotatedElement) fieldMember)); + } + } else if (member instanceof Field) { + Method fieldGetter = getGetterOfField((Field)member); + if (fieldGetter != null) { + return(getSchemaDescription((AnnotatedElement) fieldGetter)); + } + } + + return null; + } + + private Method getGetterOfField(Field field) { + try { + Class clazz = field.getDeclaringClass(); + BeanInfo info = Introspector.getBeanInfo(clazz); + PropertyDescriptor[] props = info.getPropertyDescriptors(); + for (PropertyDescriptor pd : props) { + if (pd.getName().equals(field.getName())) { + return(pd.getReadMethod()); + } + } + } catch (IntrospectionException e) { + e.printStackTrace(); + } + + return(null); + } + + //from https://stackoverflow.com/questions/13192734/getting-a-property-field-name-using-getter-method-of-a-pojo-java-bean/13514566 + private static Field getFieldByAccessor(Method method) { + try { + Class clazz = method.getDeclaringClass(); + BeanInfo info = Introspector.getBeanInfo(clazz); + PropertyDescriptor[] props = info.getPropertyDescriptors(); + for (PropertyDescriptor pd : props) { + if(method.equals(pd.getWriteMethod()) || method.equals(pd.getReadMethod())) { + String fieldName = pd.getName(); + try { + return(clazz.getDeclaredField(fieldName)); + } catch (Throwable t) { + log.error("class '" + clazz.getName() + "' contains method '" + method.getName() + "' which is an accessor for a Field named '" + fieldName + "', error getting the field:", t); + return(null); + } + } + } + } catch (Throwable t) { + log.error("error finding Field for accessor with name '" + method.getName() + "'", t); } return null; diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsSchemaBuildTest.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsSchemaBuildTest.java index 9d2b1e79f..c5bb3738d 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsSchemaBuildTest.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsSchemaBuildTest.java @@ -35,6 +35,7 @@ import graphql.Scalars; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLObjectType; @RunWith(SpringRunner.class) @SpringBootTest( @@ -120,4 +121,66 @@ public void correctlyDerivesPageableSchemaFromGivenEntities() { } + + @Test + public void correctlyDerivesSchemaDescriptionsFromGivenEntities() { + //when + GraphQLSchema schema = builder.build(); + + // then + assertThat(schema) + .describedAs("Ensure the schema is generated") + .isNotNull(); + + //then + assertThat(schema.getQueryType().getFieldDefinition("Droid").getDescription()) + .describedAs( "Ensure that Droid has the expected description") + .isEqualTo("Represents an electromechanical robot in the Star Wars Universe"); + + //then + assertThat( + ((GraphQLObjectType)schema.getQueryType().getFieldDefinition("Droid").getType()) + .getFieldDefinition("primaryFunction") + .getDescription() + ) + .describedAs( "Ensure that Droid.primaryFunction has the expected description") + .isEqualTo("Documents the primary purpose this droid serves"); + + //then + assertThat( + ((GraphQLObjectType)schema.getQueryType().getFieldDefinition("Droid").getType()) + .getFieldDefinition("id") + .getDescription() + ) + .describedAs( "Ensure that Droid.id has the expected description, inherited from Character") + .isEqualTo("Primary Key for the Character Class"); + + //then + assertThat( + ((GraphQLObjectType)schema.getQueryType().getFieldDefinition("Droid").getType()) + .getFieldDefinition("name") + .getDescription() + ) + .describedAs( "Ensure that Droid.name has the expected description, inherited from Character") + .isEqualTo("Name of the character"); + + //then + assertThat( + ((GraphQLObjectType)schema.getQueryType().getFieldDefinition("CodeList").getType()) + .getFieldDefinition("id") + .getDescription() + ) + .describedAs( "Ensure that CodeList.id has the expected description") + .isEqualTo("Primary Key for the Code List Class"); + + //then + assertThat( + ((GraphQLObjectType)schema.getQueryType().getFieldDefinition("CodeList").getType()) + .getFieldDefinition("parent") + .getDescription() + ) + .describedAs( "Ensure that CodeList.parent has the expected description") + .isEqualTo("The CodeList's parent CodeList"); + } + } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/starwars/CodeList.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/starwars/CodeList.java index 6b2f6f18d..8807b4dd1 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/starwars/CodeList.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/starwars/CodeList.java @@ -31,7 +31,6 @@ @Data public class CodeList { - @Id @GraphQLDescription("Primary Key for the Code List Class") Long id; @@ -41,8 +40,18 @@ public class CodeList { boolean active; String description; - @ManyToOne(fetch=FetchType.LAZY) - @JoinColumn(name = "parent_id") CodeList parent; + //JPA annotations moved to getters to test that @GraphQLDescription can be placed on the field when the JPA annotation is on the getter + @Id + public Long getId() { + return(id); + } + + @ManyToOne(fetch=FetchType.LAZY) + @JoinColumn(name = "parent_id") + @GraphQLDescription("The CodeList's parent CodeList") + public CodeList getParent() { + return(parent); + } } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Droid.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Droid.java index 50612bc39..b141ca9f0 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Droid.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Droid.java @@ -29,7 +29,11 @@ @EqualsAndHashCode(callSuper=true) public class Droid extends Character { - @GraphQLDescription("Documents the primary purpose this droid serves") String primaryFunction; + //description moved to getter to test it gets picked up + @GraphQLDescription("Documents the primary purpose this droid serves") + public String getPrimaryFunction() { + return(primaryFunction); + } }